diff --git a/application/pom.xml b/application/pom.xml index f454a462b9..3378d52459 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -360,6 +360,7 @@ **/sql/*Test.java + **/psql/*Test.java **/nosql/*Test.java 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 fee620c6da..a5b5146004 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 @@ -18,9 +18,9 @@ "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 },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable alarm filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableFilter\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AlarmTableSettings\",\n \"properties\": {\n \"alarmsTitle\": {\n \"title\": \"Alarms table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSelection\": {\n \"title\": \"Enable alarms selection\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSearch\": {\n \"title\": \"Enable alarms search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable alarm filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayDetails\": {\n \"title\": \"Display alarm details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowAcknowledgment\": {\n \"title\": \"Allow alarms acknowledgment\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowClear\": {\n \"title\": \"Allow alarms clear\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableFilter\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/alarm/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, alarm, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/alarm/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/alarm/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", "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,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"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}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" } } 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 de453f03a7..5a4eab0522 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 @@ -18,10 +18,10 @@ "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.getSettingsSchema = function() {\n return TbAnalogueCompass.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", + "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.getSettingsSchema = function() {\n return TbAnalogueCompass.settingsSchema;\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", - "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\":\"Analogue Compass\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":{}}" } }, { @@ -36,10 +36,10 @@ "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.getSettingsSchema = function() {\n return TbAnalogueLinearGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", + "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.getSettingsSchema = function() {\n return TbAnalogueLinearGauge.settingsSchema;\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", - "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\":\"Temperature gauge - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -54,10 +54,10 @@ "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.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", + "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.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\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", - "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 - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -72,10 +72,10 @@ "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.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", + "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.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\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", - "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 - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -90,10 +90,10 @@ "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.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n", + "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.getSettingsSchema = function() {\n return TbAnalogueRadialGauge.settingsSchema;\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", - "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 - Canvas Gauges\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } } ] 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 c4cea769f1..41aed542dd 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -36,7 +36,7 @@ "resources": [], "templateHtml": "", "templateCss": "", - "controllerScript": "self.onInit = function() {\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 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\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", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"
HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}", "dataKeySettingsSchema": "{}\n", "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}" @@ -54,9 +54,9 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"identifyDeviceSelector\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellContentFunction === true\"\n }\n ]\n}", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: 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}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"identifyDeviceSelector\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n }\n ]\n}", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"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', amount = 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}\"},\"_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;\"},{\"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;\"}]}],\"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\"}" } }, @@ -126,9 +126,9 @@ "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 },\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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", "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,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"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\",\"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;\"}]}]}" } }, @@ -145,7 +145,7 @@ "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesHierarchySettings\",\n \"properties\": {\n \"nodeRelationQueryFunction\": {\n \"title\": \"Node relations query function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeHasChildrenFunction\": {\n \"title\": \"Node has children function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeOpenedFunction\": {\n \"title\": \"Default node opened function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeDisabledFunction\": {\n \"title\": \"Node disabled function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeIconFunction\": {\n \"title\": \"Node icon function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodeTextFunction\": {\n \"title\": \"Node text function: f(nodeCtx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"nodesSortFunction\": {\n \"title\": \"Nodes sort function: f(nodeCtx1, nodeCtx2)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"nodeRelationQueryFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_relation_query_fn\"\n },\n {\n \"key\": \"nodeHasChildrenFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_has_children_fn\"\n },\n {\n \"key\": \"nodeOpenedFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_opened_fn\"\n },\n {\n \"key\": \"nodeDisabledFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_disabled_fn\"\n },\n {\n \"key\": \"nodeIconFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_icon_fn\"\n },\n {\n \"key\": \"nodeTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/node_text_fn\"\n },\n {\n \"key\": \"nodesSortFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entities_hierarchy/nodes_sort_fn\"\n }\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}", "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\":\"/**\\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 \",\"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**/\",\"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**/\",\"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\",\"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**/\",\"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 \"},\"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\":{}}" } @@ -162,15 +162,15 @@ "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 maxDatasources: 1,\n dataKeysOptional: true,\n singleEntity: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"QR Code\",\n \"properties\": {\n \"qrCodeTextPattern\": {\n \"title\": \"QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"${entityName}\"\n },\n \"useQrCodeTextFunction\": {\n \"title\": \"Use QR code text function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"qrCodeTextFunction\": {\n \"title\": \"QR code text function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return data['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"qrCodeTextPattern\",\n \"useQrCodeTextFunction\",\n {\n \"key\": \"qrCodeTextFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"QR Code\",\n \"properties\": {\n \"qrCodeTextPattern\": {\n \"title\": \"QR code text pattern (for ex. '${entityName} | ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"${entityName}\"\n },\n \"useQrCodeTextFunction\": {\n \"title\": \"Use QR code text function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"qrCodeTextFunction\": {\n \"title\": \"QR code text function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useQrCodeTextFunction\",\n {\n \"key\": \"qrCodeTextPattern\",\n \"condition\": \"model.useQrCodeTextFunction !== true\"\n },\n {\n \"key\": \"qrCodeTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/qrcode/qrcode_text_fn\",\n \"condition\": \"model.useQrCodeTextFunction === true\"\n }\n ]\n}\n", "dataKeySettingsSchema": "{}\n", - "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['entityName'];\"},\"title\":\"QR Code\"}" + "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 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": { @@ -179,12 +179,12 @@ "sizeY": 3.5, "resources": [], "templateHtml": "\n", - "templateCss": "#container {\n overflow: auto;\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.onDestroy = function() {\n}\n\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Markdown card\",\n \"properties\": {\n \"markdownTextPattern\": {\n \"title\": \"Markdown pattern (markdown with variables, for ex. '${entityName} or ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"# Markdown card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.\"\n },\n \"markdownCss\": {\n \"title\": \"Markdown CSS\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useMarkdownTextFunction\": {\n \"title\": \"Use markdown text function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markdownTextFunction\": {\n \"title\": \"Markdown text function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"markdownTextPattern\",\n \"type\": \"markdown\"\n },\n {\n \"key\": \"markdownCss\",\n \"type\": \"css\"\n },\n \"useMarkdownTextFunction\",\n {\n \"key\": \"markdownTextFunction\",\n \"type\": \"javascript\"\n }\n ]\n}\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 };\n}\n\nself.onDestroy = function() {\n}\n\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Markdown/HTML card\",\n \"properties\": {\n \"markdownTextPattern\": {\n \"title\": \"Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')\",\n \"type\": \"string\",\n \"default\": \"# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.\"\n },\n \"markdownCss\": {\n \"title\": \"Markdown/HTML CSS\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useMarkdownTextFunction\": {\n \"title\": \"Use markdown/HTML value function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"markdownTextFunction\": {\n \"title\": \"Markdown/HTML value function: f(data)\",\n \"type\": \"string\",\n \"default\": \"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useMarkdownTextFunction\",\n {\n \"key\": \"markdownTextPattern\",\n \"type\": \"markdown\",\n \"condition\": \"model.useMarkdownTextFunction !== true\"\n },\n {\n \"key\": \"markdownTextFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/markdown/markdown_text_fn\",\n \"condition\": \"model.useMarkdownTextFunction === true\"\n },\n {\n \"key\": \"markdownCss\",\n \"type\": \"css\"\n }\n ]\n}\n", "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\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"16px\",\"settings\":{\"markdownTextPattern\":\"### Markdown card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\"},\"title\":\"Markdown 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}" + "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}" } } ] -} +} \ 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 0ff51cb0fe..602fdb3c6b 100644 --- a/application/src/main/data/json/system/widget_bundles/charts.json +++ b/application/src/main/data/json/system/widget_bundles/charts.json @@ -22,10 +22,10 @@ ], "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\n", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"showTooltip\": {\n \"title\": \"Show Tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTooltip\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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 - Chart.js\"}" + "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\"}" } }, { @@ -44,10 +44,10 @@ ], "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\n", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"showTooltip\": {\n \"title\": \"Show Tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"borderWidth\": {\n \"title\": \"Border width\",\n \"type\": \"number\",\n \"default\": 5\n },\n \"borderColor\": {\n \"title\": \"Border color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"legend\": {\n \"title\": \"Legend settings\",\n \"type\": \"object\",\n \"properties\": {\n \"display\": {\n \"title\": \"Display legend\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelsFontColor\": {\n \"title\": \"Labels font color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTooltip\",\n \"borderWidth\", \n {\n \"key\": \"borderColor\",\n \"type\": \"color\"\n }, \n {\n \"key\": \"legend\",\n \"items\": [\n \"legend.display\",\n {\n \"key\": \"legend.labelsFontColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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 - Chart.js\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -84,7 +84,7 @@ ], "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", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"showTooltip\": {\n \"title\": \"Show Tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTooltip\"\n ]\n}", "dataKeySettingsSchema": "{}\n", "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\"}" @@ -106,10 +106,10 @@ ], "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", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"showTooltip\": {\n \"title\": \"Show Tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTooltip\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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 - Chart.js\"}" + "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\"}" } }, { @@ -128,10 +128,10 @@ ], "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", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"showTooltip\": {\n \"title\": \"Show Tooltip\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTooltip\"\n ]\n}", "dataKeySettingsSchema": "{}\n", - "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 - Chart.js\"}" + "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\"}" } }, { 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 7925e67ffd..f1e3eaa1fe 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 @@ -73,7 +73,7 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"Switch title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showOnOffLabels\": {\n \"title\": \"Show on/off labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve on/off value using method\",\n \"type\": \"string\",\n \"default\": \"rpc\"\n },\n \"valueKey\": {\n \"title\": \"Attribute/Timeseries value key (only when subscribe for attribute/timeseries method)\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"getValueMethod\": {\n \"title\": \"RPC get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"RPC set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"convertValueFunction\": {\n \"title\": \"Convert value function, f(value), returns payload used by RPC set value method\",\n \"type\": \"string\",\n \"default\": \"return value;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n \"showOnOffLabels\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"none\",\n \"label\": \"Don't retrieve\"\n },\n {\n \"value\": \"rpc\",\n \"label\": \"Call RPC get value method\"\n },\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueKey\",\n \"getValueMethod\",\n \"setValueMethod\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"convertValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"Switch title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showOnOffLabels\": {\n \"title\": \"Show on/off labels\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve on/off value using method\",\n \"type\": \"string\",\n \"default\": \"rpc\"\n },\n \"valueKey\": {\n \"title\": \"Attribute/Timeseries value key (only when subscribe for attribute/timeseries method)\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"getValueMethod\": {\n \"title\": \"RPC get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"RPC set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"convertValueFunction\": {\n \"title\": \"Convert value function, f(value), returns payload used by RPC set value method\",\n \"type\": \"string\",\n \"default\": \"return value;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n \"showOnOffLabels\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"none\",\n \"label\": \"Don't retrieve\"\n },\n {\n \"value\": \"rpc\",\n \"label\": \"Call RPC get value method\"\n },\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueKey\",\n \"getValueMethod\",\n \"setValueMethod\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_value_fn\"\n },\n {\n \"key\": \"convertValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/convert_value_fn\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "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}" } @@ -91,7 +91,7 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"Switch title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve on/off value using method\",\n \"type\": \"string\",\n \"default\": \"rpc\"\n },\n \"valueKey\": {\n \"title\": \"Attribute/Timeseries value key (only when subscribe for attribute/timeseries method)\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"getValueMethod\": {\n \"title\": \"RPC get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"RPC set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"convertValueFunction\": {\n \"title\": \"Convert value function, f(value), returns payload used by RPC set value method\",\n \"type\": \"string\",\n \"default\": \"return value;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"none\",\n \"label\": \"Don't retrieve\"\n },\n {\n \"value\": \"rpc\",\n \"label\": \"Call RPC get value method\"\n },\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueKey\",\n \"getValueMethod\",\n \"setValueMethod\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n {\n \"key\": \"convertValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"Switch title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve on/off value using method\",\n \"type\": \"string\",\n \"default\": \"rpc\"\n },\n \"valueKey\": {\n \"title\": \"Attribute/Timeseries value key (only when subscribe for attribute/timeseries method)\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"getValueMethod\": {\n \"title\": \"RPC get value method\",\n \"type\": \"string\",\n \"default\": \"getValue\"\n },\n \"setValueMethod\": {\n \"title\": \"RPC set value method\",\n \"type\": \"string\",\n \"default\": \"setValue\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"convertValueFunction\": {\n \"title\": \"Convert value function, f(value), returns payload used by RPC set value method\",\n \"type\": \"string\",\n \"default\": \"return value;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"none\",\n \"label\": \"Don't retrieve\"\n },\n {\n \"value\": \"rpc\",\n \"label\": \"Call RPC get value method\"\n },\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueKey\",\n \"getValueMethod\",\n \"setValueMethod\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_value_fn\"\n },\n {\n \"key\": \"convertValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/convert_value_fn\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "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}" } @@ -109,7 +109,7 @@ "templateHtml": "", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"performCheckStatus\": {\n \"title\": \"Perform RPC device status check\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"checkStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"performCheckStatus\",\n \"checkStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"initialValue\": {\n \"title\": \"Initial value\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"title\": {\n \"title\": \"LED title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"ledColor\": {\n \"title\": \"LED Color\",\n \"type\": \"string\",\n \"default\": \"green\"\n },\n \"performCheckStatus\": {\n \"title\": \"Perform RPC device status check\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"checkStatusMethod\": {\n \"title\": \"RPC check device status method\",\n \"type\": \"string\",\n \"default\": \"checkStatus\"\n },\n \"retrieveValueMethod\": {\n \"title\": \"Retrieve led status value using method\",\n \"type\": \"string\",\n \"default\": \"attribute\"\n },\n \"valueAttribute\": {\n \"title\": \"Device attribute/timeseries containing led status value\",\n \"type\": \"string\",\n \"default\": \"value\"\n },\n \"parseValueFunction\": {\n \"title\": \"Parse led status value function, f(data), returns boolean\",\n \"type\": \"string\",\n \"default\": \"return data ? true : false;\"\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout (ms)\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"requestPersistent\": {\n \"title\": \"RPC request persistent\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"persistentPollingInterval\": {\n \"title\": \"Polling interval in milliseconds to get persistent RPC command response\",\n \"type\": \"number\",\n \"default\": 5000,\n \"minimum\": 1000\n }\n },\n \"required\": [\"valueAttribute\", \"requestTimeout\"]\n },\n \"form\": [\n \"initialValue\",\n \"title\",\n {\n \"key\": \"ledColor\",\n \"type\": \"color\"\n },\n \"performCheckStatus\",\n \"checkStatusMethod\",\n {\n \"key\": \"retrieveValueMethod\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"attribute\",\n \"label\": \"Subscribe for attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Subscribe for timeseries\"\n }\n ]\n },\n \"valueAttribute\",\n {\n \"key\": \"parseValueFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_value_fn\"\n },\n \"requestTimeout\",\n \"requestPersistent\",\n {\n \"key\": \"persistentPollingInterval\",\n \"condition\": \"model.requestPersistent === true\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "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}" } @@ -151,4 +151,4 @@ } } ] -} +} \ 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 9455ac4100..aaf81bd0e3 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 @@ -18,10 +18,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -36,7 +36,7 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", "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\":{}}" @@ -54,7 +54,7 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", "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\":{}}" @@ -72,10 +72,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":{}}" } }, { @@ -90,7 +90,7 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", "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}}" @@ -108,7 +108,7 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", "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}}" @@ -126,10 +126,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "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\":{}}" } }, { @@ -144,10 +144,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -162,10 +162,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -180,7 +180,7 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}", + "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", "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\":{}}" @@ -198,10 +198,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -216,10 +216,10 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", - "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 - justGage\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" + "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}}" } }, { @@ -234,7 +234,7 @@ "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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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}\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.getSettingsSchema = function() {\n return TbCanvasDigitalGauge.settingsSchema;\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", "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\":{}}" 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 74260d3021..5fdced278b 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 @@ -18,9 +18,9 @@ "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 },\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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", "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},\"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\",\"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 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\":[],\"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\"}]}}" } }, @@ -36,9 +36,9 @@ "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 },\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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", + "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"defaultColumnVisibility\": {\n \"title\": \"Default column visibility\",\n \"type\": \"string\",\n \"default\": \"visible\"\n },\n \"columnSelectionToDisplay\": {\n \"title\": \"Column selection in 'Columns to Display'\",\n \"type\": \"string\",\n \"default\": \"enabled\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/entity/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n },\n {\n \"key\": \"defaultColumnVisibility\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"visible\",\n \"label\": \"Visible\"\n },\n {\n \"value\": \"hidden\",\n \"label\": \"Hidden\"\n } \n ]\n },\n {\n \"key\": \"columnSelectionToDisplay\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"enabled\",\n \"label\": \"Enabled\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n } \n ]\n }\n ]\n}", "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\"}]}}" } } 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 6ca5222f16..41583a4a8f 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 @@ -19,7 +19,7 @@ "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_gpio_status_fn\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "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\"}" } @@ -73,7 +73,7 @@ "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"gpioList\": {\n \"title\": \"Gpio switches\",\n \"type\": \"array\",\n \"minItems\" : 1,\n \"items\": {\n \"title\": \"Gpio switch\",\n \"type\": \"object\",\n \"properties\": {\n \"pin\": {\n \"title\": \"Pin\",\n \"type\": \"number\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n },\n \"row\": {\n \"title\": \"Row\",\n \"type\": \"number\"\n },\n \"col\": {\n \"title\": \"Column\",\n \"type\": \"number\"\n }\n },\n \"required\": [\"pin\", \"label\", \"row\", \"col\"]\n }\n },\n \"requestTimeout\": {\n \"title\": \"RPC request timeout\",\n \"type\": \"number\",\n \"default\": 500\n },\n \"switchPanelBackgroundColor\": {\n \"title\": \"Switches panel background color\",\n \"type\": \"string\",\n \"default\": \"#008a00\"\n },\n \"gpioStatusRequest\": {\n \"title\": \"GPIO status request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"getGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"gpioStatusChangeRequest\": {\n \"title\": \"GPIO status change request\",\n \"type\": \"object\",\n \"properties\": {\n \"method\": {\n \"title\": \"Method name\",\n \"type\": \"string\",\n \"default\": \"setGpioStatus\"\n },\n \"paramsBody\": {\n \"title\": \"Method body\",\n \"type\": \"string\",\n \"default\": \"{\\n \\\"pin\\\": \\\"{$pin}\\\",\\n \\\"enabled\\\": \\\"{$enabled}\\\"\\n}\"\n }\n },\n \"required\": [\"method\", \"paramsBody\"]\n },\n \"parseGpioStatusFunction\": {\n \"title\": \"Parse gpio status function\",\n \"type\": \"string\",\n \"default\": \"return body[pin] === true;\"\n } \n },\n \"required\": [\"gpioList\", \n \"requestTimeout\",\n \"switchPanelBackgroundColor\",\n \"gpioStatusRequest\",\n \"gpioStatusChangeRequest\",\n \"parseGpioStatusFunction\"]\n },\n \"form\": [\n \"gpioList\",\n \"requestTimeout\",\n {\n \"key\": \"switchPanelBackgroundColor\",\n \"type\": \"color\"\n },\n {\n \"key\": \"gpioStatusRequest\",\n \"items\": [\n \"gpioStatusRequest.method\",\n {\n \"key\": \"gpioStatusRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"gpioStatusChangeRequest\",\n \"items\": [\n \"gpioStatusChangeRequest.method\",\n {\n \"key\": \"gpioStatusChangeRequest.paramsBody\",\n \"type\": \"json\"\n }\n ]\n },\n {\n \"key\": \"parseGpioStatusFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/rpc/parse_gpio_status_fn\"\n }\n ]\n}", "dataKeySettingsSchema": "{}\n", "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\"}" } 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 249c2997b9..1b085be999 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 @@ -18,7 +18,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\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": "{}\n", "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}" @@ -38,7 +38,7 @@ "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": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"MultipleInput\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showActionButtons\":{\n \"title\":\"Show action buttons\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"updateAllValues\": {\n \"title\":\"Update all values, not only modified\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"saveButtonLabel\": {\n \"title\": \"'SAVE' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"resetButtonLabel\": {\n \"title\": \"'UNDO' button label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"showResultMessage\":{\n \"title\":\"Show result message\",\n \"type\":\"boolean\",\n \"default\": true\n },\n \"showGroupTitle\": {\n \"title\":\"Show title for group of fields, related to different entities\",\n \"type\":\"boolean\",\n \"default\": false\n },\n \"groupTitle\": {\n \"title\": \"Group title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"fieldsAlignment\": {\n \"title\": \"Fields alignment\",\n \"type\": \"string\",\n \"default\": \"row\"\n },\n \"fieldsInRow\": {\n \"title\": \"Number of fields in the row\",\n \"type\": \"number\",\n \"default\": \"2\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n \"showActionButtons\",\n {\n \"key\": \"updateAllValues\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"saveButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n {\n \"key\": \"resetButtonLabel\",\n \"condition\": \"model.showActionButtons === true\"\n },\n \"showResultMessage\",\n \"showGroupTitle\",\n \"groupTitle\",\n {\n \"key\": \"fieldsAlignment\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"row\",\n \"label\": \"Row (default)\"\n },\n {\n \"value\": \"column\",\n \"label\": \"Column\"\n }\n ]\n },\n {\n \"key\": \"fieldsInRow\",\n \"condition\": \"model.fieldsAlignment === 'row'\"\n }\n ]\n}", - "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n }\n ]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", + "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"dataKeyType\": {\n \"title\": \"Datakey type\",\n \"type\": \"string\",\n \"default\": \"server\"\n },\n \"dataKeyValueType\": {\n \"title\": \"Datakey value type\",\n \"type\": \"string\",\n \"default\": \"string\"\n },\n \"selectOptions\": {\n \"title\": \"Select options\",\n \"type\": \"array\",\n \"default\": [],\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"value\": {\n \"title\": \"Value (write 'null' for create empty option)\",\n \"type\": \"string\"\n },\n \"label\": {\n \"title\": \"Label\",\n \"type\": \"string\"\n }\n },\n \"required\": [\"value\"]\n }\n },\n \"step\": {\n \"title\": \"Step interval between values\",\n \"type\": \"number\",\n \"default\": \"1\"\n },\n \"minValue\": {\n \"title\": \"Minimum value\",\n \"type\": \"number\"\n },\n \"maxValue\": {\n \"title\": \"Maximum value\",\n \"type\": \"number\"\n },\n \"required\": {\n \"title\": \"Value is required\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"requiredErrorMessage\": {\n \"title\": \"'Required' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"minValueErrorMessage\": {\n \"title\": \"'Min Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"maxValueErrorMessage\": {\n \"title\": \"'Max Value' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"invalidDateErrorMessage\": {\n \"title\": \"'Invalid Date' error message\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"isEditable\": {\n \"title\": \"Ability to edit attribute\",\n \"type\": \"string\",\n \"default\": \"editable\"\n },\n \"disabledOnDataKey\": {\n \"title\": \"Disable on false value of another datakey (specify datakey name)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"dataKeyHidden\": {\n \"title\": \"Hide input field\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"icon\": {\n \"title\": \"Icon to show before input cell\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"dataKeyType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"server\",\n \"label\": \"Server attribute (default)\"\n },\n {\n \"value\": \"shared\",\n \"label\": \"Shared attribute\"\n },\n {\n \"value\": \"timeseries\",\n \"label\": \"Timeseries\"\n }\n ]\n },\n {\n \"key\": \"dataKeyValueType\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"string\",\n \"label\": \"String\"\n },\n {\n \"value\": \"double\",\n \"label\": \"Double\"\n },\n {\n \"value\": \"integer\",\n \"label\": \"Integer\"\n },\n {\n \"value\": \"booleanCheckbox\",\n \"label\": \"Boolean (Checkbox)\"\n },\n {\n \"value\": \"booleanSwitch\",\n \"label\": \"Boolean (Switch)\"\n },\n {\n \"value\": \"dateTime\",\n \"label\": \"Date & Time\"\n },\n {\n \"value\": \"date\",\n \"label\": \"Date\"\n },\n {\n \"value\": \"time\",\n \"label\": \"Time\"\n },\n {\n \"value\": \"select\",\n \"label\": \"Select\"\n }\n ]\n },\n {\n \"key\": \"selectOptions\",\n \"condition\": \"model.dataKeyValueType === 'select'\",\n \"items\": [\"selectOptions[].value\", \"selectOptions[].label\"]\n },\n {\n \"key\": \"step\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"minValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValue\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n \"required\",\n {\n \"key\": \"requiredErrorMessage\",\n \"condition\": \"model.required === true\"\n },\n {\n \"key\": \"invalidDateErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'dateTime' || model.dataKeyValueType === 'date' || model.dataKeyValueType === 'time'\"\n },\n {\n \"key\": \"minValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"maxValueErrorMessage\",\n \"condition\": \"model.dataKeyValueType === 'double' || model.dataKeyValueType === 'integer'\"\n },\n {\n \"key\": \"isEditable\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"editable\",\n \"label\": \"Editable (default)\"\n },\n {\n \"value\": \"disabled\",\n \"label\": \"Disabled\"\n },\n {\n \"value\": \"readonly\",\n \"label\": \"Read-only\"\n }\n ]\n },\n \"disabledOnDataKey\",\n \"dataKeyHidden\",\n\t\t{\n \t\t\"key\": \"icon\",\n\t\t\t\"type\": \"icon\"\n\t\t}\n ]\n}\n", "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\":{}}" } }, @@ -72,7 +72,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\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": "{}\n", "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}" @@ -414,7 +414,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\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": "{}\n", "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}" 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 b7fde746f6..f6b4d75c3a 100644 --- a/application/src/main/data/json/system/widget_bundles/maps.json +++ b/application/src/main/data/json/system/widget_bundles/maps.json @@ -18,7 +18,7 @@ "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.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\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.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map', true);\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": "{}\n", "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\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details
\",\"markerImageSize\":34,\"useColorFunction\":true,\"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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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"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;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d3\",\"tmDefaultMapType\":\"roadmap\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"tencent-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Route Map - Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" @@ -54,7 +54,7 @@ "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.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\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.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map', true);\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": "{}\n", "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\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"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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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"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;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d2\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"google-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Route Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" @@ -72,7 +72,7 @@ "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.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map', true);\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\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.onResize = function() {\n self.ctx.map.resize();\n}\n\nself.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map', true);\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map', true);\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": "{}\n", "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\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Speed: ${Speed} MPH
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"markerImages\":[\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7b13uB3VdTb+rrX3zJx6i7qQUAEJIQlRBAZc6BgLDDYmIIExLjgJcQk/YkKc4gIGHH+fDSHg2CGOHRuCQ4ltbBODJIroIIoQIJCQdNXLvVe3nT4ze6/1/XHOlYWQAJuWP37refYz58yd3d6zyt5rr1mX8B7S5Xo5/0nPYaNFM1PY0gGqOhfAgQCNBGlWFFUAYEIeihigbhFdZQwt85BV5Gj9r/718R2XX365vFdzoHe7w6d77xnPkn4YpAtU0YiizNJcmPNkMQFkDiSlowHt2HNtGlTSJ6B+pTpsKTfKgTj3Pi8SMtFtEZnFs8d8dPu7OZ93BcCHtt0+OiL+FJjOiqy5K5dtLwD4PBHGvy0dKLYo8B+1+lAldv50FfmFzWX+84i2M3a8Le2/Dr1jAKqCHtl2y1wC/pEMP9ZRLBaYzF8CCN+pPluUkOKfB6qlmk/dBwTyt8eOv2AZCPpOdPaOAPjA1h9/SJX+TyGXuz0TZi4EcPBeOk+U+RErZh2YyMAyQJEoZUjFgtkCAEScgDyx1hmInTglqDj2U1X0WILaPbWvwHO1WummeuLONhaXHTf2wsfe7rm+rQDe133j/i5xPyrmCr+OouhSKPbdQ5fLiezTIYUBQGMJBgYWxMYSISZhbxgQT8wGAgDiwWxUvCiBxKhSKOqdh4OyV5+6XiEfK/kjVOXQ13apG+I0+adKpXaG0/Si0yZdvPbtmvPbAuCNT98YTBhT/8fAmEpHoXgKgPe/6gFGP0nwG8s2YykcaRCAYYQ5tKTkDVuArDEwMRF5AICS4VZ1AQBSr6oEgL36CBAvlKqIsyLOKQl5TZH4uN+TawDuY6o64lWTJX20v1S633uJNvfmvnbRERelb3XubxnAX26+5gDy6Y9HtrU/wERff1XjSt0WwULDmZEMawPOgilgQ4FaGCEygaXQMQyRMaxiUijUkAEAImIGAFURAOrVA1AmI1ZExGuqoqkVFefhyGtKDql4X4eHc6LxJof0VIVM3nVc4uXaHUPlo0Tpc2fv/zer38r83xKAd6y74iImO31EMf9REA7cpdVBY8NbA5+dFNqsCTQipkitBjAUsLUZNd4qm8AyjDMmJAIRhDzDEBEbJkBVAyJWQJ14AEaciIeSGicOgBeBWNHEeXLkXIM8UvFI4bVBCVJNfdk7STd5xOcp0LZzjIqV/eXq/4i61edM/eaN7yqAqpfzf62Nf5LP5lbko/DbCuxU4saEN1mN2kKTzQbIkuEIEWfVagRDEVkOyXCkVq0aDg2p9YYNAySVerU0WN1R27Jjo6ulMQ1V+ggAOgsjNRNEus/IiUFnYUy2kM23AcrivXh2RiTxjhx5iSmVWEWdpmhQ4qvwSBBrXVPfqDmuVsT7C3aZvKslyZcr9dpxdr81F8ynO/w7DuD1q/8y6kDw2872ticN0deG7wvQHXHmdxGK+1ibQag5ikweliIElNUAEayNYBCSRQRiYzf2rNtx11O/rC5d9dj+1aQyM2Pyz3WGozaNisYNWY7SYtgWA0A5KUVO4qAn3t4+lOzYt+Grh+bDwstHzvjA2tPfd1Z+39FTRhGpi7VBKrE4nyBFDKcNJL5OCerqUEXdVeEQb0mk8lECjR0euxe9cqBUOnoQ6RkXT78hfscAvH71X0Z5kf8Z0dH2CgNf2NkI0d0ZbmtElMtFVEAQ5BFIlkKb00AzFJqCGooQcJjv7t868P3/ubayZvua48ZlJt57xLjjB/cpTssXokK7IQNrbeoZ3pIRJm1aYSUW9cwixglZ7xNU40ppY7mr+sy2ezt7G1s+vP+EGfd/+fS/Ko5pH9/pJK04X6MUDSRapcTXkXJN46QKp1UkqNVqvpxVyLzhOajihh1DpVkmrJ7+uak/bbztAF6/+i8j62p3j20vbgXR+cP3LYU/Djg/KcsdEnIWERcRIk+hzWtEOYSch2U76tk1T6+84Tf/NCdni2tOmbRgy6T26WOiKDBhGFEQhrBhiNAyjDGiQp4DFgI8AChg1BGBXOC9p8QJ0kas3jvEcUxxnLgNpTW9izfdOqGWlve7+OOXrThk6qEHKtKehq9xIlWkvoaYytrwFYqlglgrcZxW+oXSz+ycpOLmnsHypDTIfuTNcuKbAvD2288x22dn7hrVnt/ATBftBE/CH2aCtqkZU6CI2hHZomS4YCPK+5AKHFB2ZNe2Nev/739/e9qY3KRnPzHtQp/LtnfkMhnKZDMa2oDCTIjQhghDC2MCCQITAyYxpmkhAIAZDDA7l4bOSeR9YpLEwfkUjXqMOE0QN2LU4waq9aGBX6/+d7O9sXnu3579jbVTx02dlEilL0FDG1pJG64cJX5IGr6MupY5duU1npIv7sTQ4196ytUDx8+sf+TN6MQ3AyBd8+L8W0a15zYw0d8O3ww4vC7ijlkZU5QctVPE7QhNEVlTRNYUjHcy7tu3fuuVSqXBF8z66962fMeIfDaHfD4nmUyWsrk8BdaYIAh9EFoxzExEysYoAQ5A0ioAEIpIBGZmAM459iKaJo6cT209TnyjWkOSNLRWi1GtV9A3sGPg56uvG1vIZ9N/OO9rM8jS9oavSOwqaEhZYh3khq9K3fdpXWsbvdR3MoYCV/UOVadcOvv2C/AG9IYAfue5j1/U0R5mIhNctxM8yvxLyMVpOduJyLRRnto1MkXK23axlB27sXtT1z//8vqDTt3vk/fMGnX4xGyhiEI2Qi6X1Ww2S7lCIQ3DkCxzQEQKYADANgCbW6UHvwcRaO6fAwCjAewLYAKAcao6UkRIBEniEtRqNVOrVKjeSFCP61oaqurKvqe237P2lnkXn/X/PT9l3OT9Eql2V90QN1wZdRqSuhukhi9T3Q2s9ki+NDzHWppeUqnG/qsH/+b7fzSA33ruI7ODIDh/RCH6KkEZAEINfhia4n4ZO0KzphN5005Z06aRaeOAcjP++4Ff3P/86hWTLjr08i3FfEeurS3LUTanhVwe+XxOwjAw1loLoB/ASgBrAdSAV232Gc0NyJGt70+27mlrzNT6nAEwDcBMACO892kcx1KvN6hUqWu9Xka9XsfgUP/Qjcu+Nf3g6bO7zj7urBNT1F+quxLXfUkaMmDrviQ13+8THdqYqvuLZpfq+qrJNXFDbrp87t0v/cEAXr5iduiTMQvHd2QnKDC9+bC9NUfF9kwwgvNmBGW5Q3O2SFkzAkaCg/71Nz9+2MTZ6rlzLs4Vi0WbyWS5o63N5fM5G0VRaoxpA7ChBVw3ANMq1AKoHUAewCwARwHYvzWctQCeaNUrt4pvgeha17Gtevt47+M4jrVSqZlSqepqjQpVyyX/8xU3VBHF2T//+OeOFbgXaq5fa75ENR3SarzDxDToYz846FTORbPRV7oHG9sm+qEPX3TEM3vc9pm9AfiBP53+T6Pbwo0Cd4aog4p/yXK+lDX5IDIFZDinGS7CckEM+JB//u9/e3Z8NGPTgjl/Maq9s8N2FNtcPpc1bW1tFIZhaIxJATwFYA2AtAVWh4hERBQByIgIE1Gsql8gou8AeAjAfQAeVdUvEtE9reFFIpIloiyATgARgCqALQAGmHmUtTYTRWHDhhaGYE0YYmbHEXZj//rBRc/fXTly5qGHEus2FUceCbxP4DShRJ2mvuIFboyqG5kNcNuWVM965MbNd71pAC99+vADA+MnR6F+TeAg6h1TeE/I2bbAFjVLBbJcpIDzZNke8qNf//yxKblZWz42+9Pj2opFbutop7ZCQdva2hAEQZGZXwGwDEBDRCJV7VTVfVV1BDNPUtXZqnomER2tqi8S0REAzgJwUqvMI6JBAM+p6pdU9f1ElGu1E6lqUVVZVYWI6gA2EFFijJmSiUIPsDbXmGT3b59V6Kv0dd334uLGYTPmHK7Q7lRi65DCawqviXWSrEm1PlvgWMh9KPbut+/77Ohtj/97d98bA6igo7aM+O/Ogp0l8BNFPQhyY2RyE0MqcC7Ia2jyGpksBYj2//WDCx9uk/EDZ8783JhiW5HbigXpaG9HNpvNMXMGwAoR6SWiUKS5KhERS0QqIgmAHcz8sqrOA7AdwCcB9AK4CcBvAdwP4EVV3V9VPwGgC8B4Zv4PIqqoqgPQYObEOadExC1A60RUJaLxURQaZqoRW0NEsm/xgI6u7rV9L295vmvGlKmHQ32vk0QdxfA+oYTq+Vgbi70mR4p6BEaKlTid98S/9f4MV7wBgF/66AEnFbPUz+z/VNTBiywLgxxCFDgwGQqR5wznOeR8+6p1657r6uopfu7wv4mKbW0oFvIoFovIZDIBEXkReUlVG6o6Fs2N/EjvfSczj2Hm/YnoY6r6Ae/9w0T0cVXdSkTfE5FsC8iTAZwI4DAAjxDRj0TkUABTACxS1csAzG39MHlmzqvqGCLKt1xZA0Q0QERtQRBkDZMngrcmNAeMmB08uHpxNsrz2pFtbft4TWInDZtSLE5T8i7uSKRS8XDjBX4fYbnusI2jMkt/tGP9rnjxrl+gICP4Riagrzb1ssKa4CkrYRhwwBFHYGSUOZJKo8oPP/vCoV846opSoZCnQj7HxUJRMplMgGblR5h5wHtfbE1oZAvIHBFtVtX7RKTQ4pSrnHOXAThQRK4BcIaqNkTkRRF5UVUTVf1462/TVPVSEfm2974qIm3MvBhAl6pGAEYAaBcR45zLiUiPiDxKRC6bzZpsNhtGUaj5fIG/dNTltYeeWja3ltbVcGgMZX1IWbUUqDUBbBA+OYxDPuDLSORq6KsN76s48MvzZnwwlzNDgaFzAIBAi0LKtGVtEQHlOaQCQpOHoWDWL+9+ZODCuV99cnTbmM5cIY+2JudZIpronHukxUWemavOuZIxpuG9H8fM8wDMJaJHVfV0ANcDOIyIPg5ghTHm+0S0UETWq2oCoA/AI6r6C2PMgyKyD4BPM/MggJ8COIGIFqnqV1T1YADbVXUjEfUaYxrOOcPMBVXdCmCutbZirQGIlIBwavucl2577NaJM6ftO1nJ9aY+YfEpvDryknamSNdAMQ1AGwxdc/DqDjz9k/7Nw5i96ixBSK/MhTRxJ7oUbracmWAoVGNCtRSCYOxLazfcN7VjdjK+beK4KAqpkMtpJpNRABNVdT2AowHUvffjAYgxZpNz7hUiuk9VT1LVWFX/iojuBfA1IrpfVRcS0Xne+6tUX33+M/zdew8AzxljLvPefxTA3xPRIufcpQA8EYUAFhPRSCKaKSL7EFGgqjtU1RDRZmaeGIbh1sh78s7LxM59R09um7585fqNdtqUMZOMMc4igE0DthSppcYWL80VTNbyX1QCPgNN1fJqDvzi0tnjQviObGia3Ee0JEAml+E8DOUo4pxaE4GUJz3yxJr9/vSIv+8uFAu2kM8jl8vBGNNJRE+q6grn3AZV3QRgi6q2AZjHzHNE5FEAp3vvv8HM8wFQSywvADAPwDgAi0TkPwDcBWDhcFHVh9FcXH9ARE4BMI6ZvyEiHwYwSVW/CeB0IlpERJeo6hwiepmIlnrvVzLzemZex8yDzDwZqlUikGGm6R0H66+evuPYafuNynvFkCCF4xjiBd67otN4C4GmEDAqTuVnR3++beWT/z5YfRUHio8/0dEe7DynJTUvswmmEiwxWcCDwGyee37j4ydNO6ucy+YmZMJQM5kMWWvHqmqPc24eADCzENEGAMvTNH2AiM5Q1W1E9GkR2cLM3yOiS0TkO0R0lao+zMy/8N7PBHAmEZ2C3YiIoKrdqnqjqq5i5j/x3n8bTQt8iapeKyKbjDGfFpEhAGOccw8EQdBhjPmQqk723rP3PrTWvhxF0Xgi6vHeayaTyx075fS7nlvxcPGgg8ZNIjHeSKRMdbEUIEHwEuCOA4DOvB25vSRnAfghMGxEFNRb7ZoM0HFNadFeIjvRgMFkhEDKbEl8Oqq7u3bs+/c9cXQUWo2iCGEYsqrG3vvHAPwEwL2qulZETnXO/Zm1FqoKVf2Bqh6qqr8SkW3e++tU9T4i+ntVnem9vw7ARQA6ReQ5AL9yzl3vnLsewK8APIfmovkiIrpWVWeo6t977x/w3l8nIluI6Dcicqiq/quqgpnJOfdnIvJR59wmEVlCRD9S1QeJKLHWmmw2hyAM9bhpp47q7q4d733aSVBlkBoNQGxgYPdVRZ82N5In9lS7dp42GgA483hMyUY0RXgwXzAjQgUtshp1WhOR5YgDzoiB0U2baqsPLB7z0oxxBxWz2Rxls1lh5gNVdbn3/rwWR68moi5VPZWZt4nIvgBGquoRAH5BRH+OprH4oYh8XlVPQXMvfIOI/BJAFxF1qupxRPRBIjpKVSe3dOtdInKbqj5PRIe3RHayiHydiMYDOIuZfyIin0HTfI4kIgAYa4y5UUQaAI4QkY8ZY5YR0aGq0kcE8k5NNS4t665u6G9r47xDCi8pqabsNbFe9WkoRvU0upYl8GunnqebX7kZQ00O9DipLbKjRfQTPWnXYyBTBxMBBiIML2IVkt20sf6B46d9rJjJ5chaQ0EQRAC2pWm6VlVXq+rZIvIXSZKELcX/Y1U9RlW/AWC8iJyqql9V1aOcc99W1SXMfAmAh1X1qy3O+rKIHCMiGRGptUqude9iIrqWiC4brisiDxHRt1X1KFX9qnPuowDGe++vUNUPishNLQkIiOjPVPVs7/02EVkLYHsYhtYYg0wm1FNmnZPftKF2lFPJisCIkhE1DFiFaNLr1i5R+PntGR5lFMcBLWfCxxbhrgkjgqMAjCKgkrWFX48KZ7RHJm8CziJLOXJpUNu4omAuOfbKOMxkKBOGHIbhHBG576qrrtLHH3/8QmaOdtdd/5tIROLTTjvtyc9//vN3BUGQs9aOA3CyiDxXr9dRrzfo2gf/Ljt1TpyYIMnWtQ4nVW2kNd+bri41fOlMADkQerb1p4/f+WGcaS9X8HOLUQIwCgCUdFGi6ehBt7k+3k4DqQ8cOd2+mQdPnP6xijHB+MAYhGEoqppL03T/J5544iRmpvnz5z+4Zs2a1dOnT5/+8ssvr5o5c+aMWq1WSdM0VdXORYsWHW+tXXbmmWcONV2jQG9v744dO3b0jR07dvSIESNG3HbbbbNFpHPBggWPtMTvVUREWL58ee2VV145bcSIEU+ddNJJ1RY4unLlytXTpk2bEoZh2N/f37dw4cKTrLUdxWLxvnnz5pnf/e53unDhwhPa2tpWnnfeecekabopCIIMEYGIyBjGCfufvmbpltuKY6a4LKkzCh8PpZu913g0oIsAOhOKMQTElyvYPrsY43IRP6uK8wCAYHrUo+gpiXoaG+LR0X5VaNgxNEAHz5pz6PIgMGBmBTCKiJZVKpUjjDEmTdPG/PnzPwSgLCJHoLlY/omqXgLgWSJauHjx4uNPP/30obPPPnsAwGNoLl+O32Xdt/a3v/3txnK5HM6fP/+3aJ2JAAi89zkAUwGcdOqpp+YvvPBCnH322fEJJ5yQA3CH9/5YY8yft0C+SkTmP/roo72NRqPjhhtuODCTyRTPOuusRy+88MJVd9xxx8cWLFiwiog+oqp3ARgVBMEO7xVzJ70/v2jdHbNGqu/16uq98WakmuQgANhsU98MRQwMP7N0iYxhUuybD/n3WzqlAMROROElzfY3NrXHrtTNFHTkMvkiGQNiZhGZ7ZzbPDx5IoKIXK2qZzDzd9F0T/0pEV2qqoeKyN8BwLZt27ap6hmq+l0RmQXgZhH5iohcpaqrwzA0RATn3DXOueta5buqeoWqnqWqT9dqte8DwPbt2zeKyBGq+l1m/giA7wL4map+jYj2S5LEA0AYhp0AvsvMp5577rn3Axi/YcOGxaoKEdkCYBYzqzGEMMgUWILRjXSopzfekFUf5wUKYXYQCoZhykcM08C+DMUMw7Rva8sHqHZCJFD1VtTDaYLuoe3xrLGH/Yu1NiZVtcYAQEVVy7vpmPNU9VHv/RUArgZQ9d5f473/qYj8OwBMmDBhPIBnnXNfAfAj59w5AK4F8DURmcfM1JrY/4jIrSJyq/f+XlV9vmVMPlEoFC4GgM7OznEicmPrB3hJRC4Tkc+IyI+897cFQWBay5lrVfVKVX30lFNOOUZV/aJFiz7YMi79RFQiIgbg2NrazHEHf7+70q1eGiwkROoteQkhOmIYp8DQBGUcYIVwOJMepCCAkBCooCAnUPVwXoU1rrXVoyi7nwgoDO1QyymwzTn34d7e3p8B+NsWFx4AYLP3/l4iuoKIHhaR/yaiLw1z6rp169Z57+cR0bUiAiIaVNU7ReR5Y0xcrVbPbf0ek1U1DwCq2qOqG4jofhHZUi6XAeC7IkIAvqCqIKItaG4LZ4jInxERvPevtK5fY+b7W+0eBGD78uXLx6nqd51z85i5G0Bore1rNJJsxuan1EumFo3w3mtKSupAMASNRJEACBk6ixWphWCaKs1tqegVUIWyiBcPIYhRQlLKhQccNDtW9YEIh0TkiciJyGFtbW29LfCCxx577PtHHHHEhdbabd77bzLzFap6jPf+X5o46Jf333//qWh6kP+P934HMx8F4HQA53rvkc/nl9frdYjIQbsw99SWy6opPvl8BQC6u7u3ENFfq+poVb1IRK4iIvHeX7dy5UpKkuR8Zka9Xv9WNps9n4j2B/DNkSNHnrV9+/ZRIvIhIjpMVZeoqlfVEcyQ6WNmpQ8+nyva9m4IO/XeQ1XFE6UKfYkUhyrTEVDEFkAWO4NuZAuAsPnDKlgFzih8ku0cU5y4NQiCxFrLAPYDUCOizxpjrgAAY4y54YYbvtwS5f1E5B9UdSgIgloURR8BIESEO++8c8qmTZtetNYeHYahdnR0wHv/pIhsrVarvX19fQsA5H71q1/dYq01pVKpkCRJXCqVaGBgwDcaDdfX1zcRwDELFy788JIlS96XJEnBOQcADSIKmfkSIsKwpXfO/bmItBljLlHVa6dNm/bIE088sR+AMUT0WRG5kIgmWWtfIWPcuPZJDJ9r90hIRVTEq5KAlBIIdYH0UCg6FMhZUvDvjSDVnZBhUhUSUijICxHCbDFXZGOMqKoH0KmqQ/l8/ptdXV0/rlar38rn8zs5hJmJmUM0jyPb4/j3h/ze+ylLly6dgr2QaepX3Hnnnefv7ZmdoyUamyTJWABoHvTtmbq6un4xa9asSQCuA7DSWvtSo9E4zHt/dbFYvKLRaKwF0E5EwoBENlKVMOPFkcJDCRBVUlEloLQTLgWz1987FAhImCECJVEh8Z6cdzBk20ITkIg4Y4xX1ZFoHuJM3XfffT/S29uLLVu2oFKp7HQ9/W8ia+2RzHyGqv6TiPzjsccei97e3kxbW9uZACYTURVNb7mIiIYmJIOwLUWqTqQVIqFEDFHV6nC7orDMBB22LOzhWbRC0LJRLalqGYqyQWAJVDPGVJIkqQPYrKq9AGCMmQoAaZpix44d2Lx5M/r7+5Gmbzn4822jVatWvei9/9M0Ted77/9j5syZawAk27ZtswCgqt0AtohIzRhTssZWDdvQkA4RtETaxAOqZSWWnXgR1Kr8/kTbG2ThtaAE9QQSZWIQ2EilFteyhoJCa4lxYMvf9xry3qNUKqFUKiEMQxQKBeRyudcVsXeC0jRFrVZDtVrFzTffnOnp6Tl2/Pjx944ePXrt9OnTzyGirY888sjLCxYsOERExhPRDGvtswACrz4m60pOqIMIBIX4ZqCYAWsZLXumAtid6z8A5DSvlgkKFkcMiBERqHUDiUu8994SkQCoEFF+jyPfhZIkQX9/P/r7+xEEAbLZLKIoQhRFbzugzjnEcYxGo4FGo/EqCejp6Tnv5ptvfk2dH/zgB8sWLFgAVS0CqHjvyTlnq2mFYF3VORnJICKwI2IFI0Qi7TCtLaYCVgnbAdoA6GRhaoPXhipIVJkEUCXP7CrleBAd2RHsvYcxpopmfMreaICZN6LpQWYRmZSmaeeuk7LWIggCWGsRhiGstWBmWGuxqwUFABEZ9ilCROCcQ5qmcM7BOYckSYbd/XuiTczcT80YHHjvZ6MZZ4O+vr5hx+14Va1Qa/M9WB0Asa+SUCcIRuAtg5QEBKDYrEJrwdhiIXhBRQyIJkMxQxQvkELh4RUq4kCJ2VHdOLiOx+YmmTC0trWwnQOgsvtoiegFInKdnZ3rRo0aJT09PTw0NAQAm0VkzvBzw5N/B0mMMU+pqhk7dmxXsVjkzZs35xuNhojICDSPRpPt27c/WSgU5hLRC95722g0aOPgWnbcW5VUBYCSJYBBChgQzWnt2J4BsJyheFkVr7Q6Hc2kZYU6ARSejCjZFN259UOrc6reOucMEfWpqnXOPQIAhULhN8PgMXNl3rx5Y4IgOIuZz46i6KyTTz55JBFVmXnFO4nYrmSMeTKKooEPfvCDs40x8621Z3d2dp566qmnxsxcArC1s7PzkVWrVi1X1QBAv/eeiYg2DK0upOgpiCBQIlIBBOrBOgTCCAAQ0jUQrGS1WF1vUPewLlTlKoQCOARewOqVUgzmtlXWTWuKiqiIVAAgjuOtuy1bgtNOO21ET0/PhO9973sQEXznO99BT0/PxJNPPrkDQAO/97C8k7RBVaO5c+ce19nZmb3yyisxZcoU/NVf/RVWrFjx/kMOOWQ9M3dXKpVRjUYjbKmGinOOnPPYWt04PZGhjHoQCZigAQsFpFwbxqlRpx6k6LI6gK5Kpz8zm20d0JHWQFAYTSUlALDexSNdEB+Y+nQxpZRlppSZ4ZybdPvttz9QqVSOt9Y+SkR+xYoVxx522GF4/PHHceCBB2LZsmWYPn06nnrqqQOZ+REiekZERr+T6BFR37hx47rWr18/NwxDvPLKKygWi3jhhRdw5JFHolarzXvuuee60jSdYFordxFJnHNI0rghiGc4jb3xUDEQEngyYEBrwx7KcuJHZzux1t79KZQ++iv5AHTnCadVBZGQhULh1SsIMfoe7KlsGRqTm5Q1xmkQBJtV9dijjz766f06bwAAEgVJREFUnpUrVy4EgIMPPjh300034bjjjsOaNWtQqVQgIjjqqKOwZMkSzJs3b/Xy5cstgFUA3rZF954cr6eccsrYxx57DJ/85CexcOFCDA0N4cQTT0S1WsWjjz4azp49+4l6vc5Tp049TVU3eu/hVXVbZUN/TH33k8c4DVRIiMFEohCjCIdXLC6VY+44DV+zACCEXiiWgnCkEp1EpKsEqqTEIsTq1Axg+eCy/kczp+QmqDZfuXpRVedNmjRpx9VXX32hiEBEsHTpUtx5551YsGABnHM47LDDcNNNN+GAAw7Al770pc8NPzdsUXe1rsOA7n4dBmjXK3NzgbHrZ2beWQDg7rvvxq233oqLL74YS5YswY4dO/Dkk09i7ty5uOCCCz4bx/FPRGSUiNydph71ap2W9T9eGGgsr4iqZSVVsLJ6Z5lIlU5srfmWAlgHtE7lDjgP5SjgAWb6MBTtoroMgpwoERTwniiJhwq5aPrxB+YOWwuQIaKEmWd573NBEHSoKosIpk+fjltvvRWqitWrV6O7uxvLli3DV77yFRQKhVeBtzcgd/2+exmm3bl3dy4kIowfPx4LFy5EpVLBpk2b0Nvbi+7ublx22WWw1ro4jgsARgJYVq/XUG/Uk2fK95+ypXxfrESGGUIEMhYGTP1ovQOYOr2+kcjvVt+K9c130cp4slyX4nDnBqYbRCAGkTZXUELIVtPeezeUu3rjOEaSJFDVpwEcmKbpLcMTnDhxIm644QYEQQDTPDvBNddcg3322ec1IL1e8d6/qryZOruDffTRR+PrX/866vU6kiTBAQccgOuvvx5hGKI15hki8lTz76lura/fUUt6F4siJIKCiREAakhB6BnGp1ST9lwbngJ2CfE99Zd4cPzIcDqg4xl4wQl64EE+BlyicCnYanHz4RMumviR9vO7C4UC5fN5JqKzVfXlKIomtzzGr5nwGwGwOxe+ngi/ntjuXowxe/s+0Gg0+ohofxG5o1KpoFqv6+LBn496dssPt6dcmWAtlCOCNRDKgJgxEopDoLRl60Cy5p5P4Hhgl/A2NbgmTuUGBeCBOUTokVZAtyiIFJSk5QmJlJKeyvaeer2u9XpdVPVxVZ1Zr9dv25PI7Q7M3sDbEwe+0Q+wt/b21vdwqdVqv1XVaar6eJwkqNdj9JY3bW9IKU5cZRwUDNPcuagBE2G7Kg5RAKnI9SD832HcdgJIARYOVdyknXtjoTpBoaRsTPOMHQy7fMutQy/qQzOr1arW63VNvd+kTc/NfO/9I3vTXXub0N5E9/U+v57Yvp7+VFWkabpYVc8DMJSm6aZyqcSNRk1fxOMHPb/5v+pQtWwgUBCxErGCiOJhXHYMuRkU4r7XAHj3aYhTAaC4rakI9dNkMMSWPBhMSsRKmjRKIyuuZ3Bzfe32crnGlVJJReQ+Vc3HcdyuqgPD4re3ib1ZHfhmVcDuYO4JxNaYetI0HYvmMen91WqVqo1YNqVdW2uutz9NSp3KTNpcxMEYgjEYVNULmvVxiwLVu09D/BoAAcAZXL6j7F9SBVRgiUwPkRJYCQaqrEoMWrrqp4WN2ZfmxXGtWq7UqFwuJyJyP4A5cRw/qKryelywNw7ck+58I336ZvtR1Uaj0XgewMEicl+5XPblcpXqtXJtk33x1KUr/6MAbnKdgQKsDFUVMTtUYFWBvpLvohRX7orZqyJU192K6tSz9Qv5HPcQaCpBZyvjRSiyEFIVkDioiBbL1W3LglGduWJ9LKDExnAtCIJEVU/w3t/MzIfsbiD2dn0jHbkrF+1qSPZkXHY3MMNX59ydaB5ePdNoNLZUqlVfrpSxOvO4earr5xvqvm8iGfggBFNIyiGYQwwQ4xwABqqLhmo+c885eJVf7NUx0gDE4iv9Q/JYc1+MDABvDJQs2DDYhlBmxD2Da6YNxOulW9dsr1TLWiqVtF6vrwawXFU/7Zz7TwB/FCf+MUuW1ylJmqY/F5GzVXVZvV5fWy6XaahU5q26asuA22L7hlbvR4a8NVAYKFsgMBACJZDm7mNHSZ41HpfujtdrovS7bkV58p/oRwpZ8zIIhwM0C0SLoBipCmqNnaHAhq3L7MT9D9mfhjIrrYRt3nu0fG9VAKd673+Npq8t82a5cW9ADdOb4bZdljfbRWSpNt9BeSJJknVDQ0MYHBqiwXRHd9+IriPvffpa4YBCE0I5grCFMRlSGFoF4DMt3ffDUtXLPfPxyzcEEADGnoNH01gWFLNmChQhgTJEOqiKQIQEAiPNU09+Zf3jfZNnH3yY9mVWasoFL16sMWVm3gzgNO/9KiJaq6qTdlfyewNv9+f+QNCGPz8qIgLgaFVdVK83egcGBk25UtWBel9f/4Q1x931yFUbYLWNIxgOoDYgDSJYE6IB8CEEjFKg1D2QdscVfHn9r/EaB+YeAdx8B9z0+Sgz8HxgeR6AMVB6hgzaVMk3Q/2JSQHvJOra+GTXlMPmfEi6o+d87NpTLyTeN5j5ZWae6b3fV0RuIaKZqmr3ZJ33BNzuAO4G0B7vMfOQiNyqzcBN8t7fN1QuN0pDJVQqJe2v9u2oTt9w0l0P/uNz3iQjghA2CMmEGXgOCSYDIqJuAk4AgHrDf7We6u/uPx97zO6x13fl1tyOtfucqRcXM+ZFAHNAmA2iu4gwRkBKos0jAVXy4vKvrHvslWlHHHZk2m1eQKJ5VfXOOauqG4Mg6FXVj4nIalVdpKoHqSrtsrzYed1VXAHsDaQ9caAQ0S0iMoqIPkBEDzWSZHWlXI6HBkvBUKWsQ2nf5uSA7SfeueTqFxPUxtpQAxMSmxBqAhKTBZhoBYALAUCBW3ZU/D6Lz8E1e8NprwACwKQv4nf1fvlUMWsJwEgC5oDpIVJ0EhGrJ6sAICCXuvYVqx8uzXj/YZPSWFbWelyHeA/nPRLvqwxa3XRN4COqugrNKPwx2ozifxVww1y3K4CvA95WAHdQ8xWHDwJY4b1/tlwupwNDVVTKQ9rfP6j19h3dsv+Ow29bdEWvUmO0CWBshowJCTZL3kQAW1pPTb1noPTK9oG0no7Cp9b/7LWi+6YAXP8zuMnn4rFG4kfnQ3MYgIgIU5jxDCmKCigBpE1xZlEfvPDSErffrFkU7BNQpSutxQ1PLo6zSerFi9RV/CvMXFXVQ1R1H1VdhGaIbxnAzgQ5u4vtLsUx8yMA7mPmbQAOJKI2VV2XJMlLtVqtViqVaLBUlUqpn0vloTofOhBVMptzv1h4dd4Yn7cR1GSJwwhiQhIbIjUBthBwJoC8ElzvUHqzKL5+/+l4zQuGu9Kbyplw4m04Ix/xjI68+W6r2gZifdI1dFSaEEtdOW2AJYG6hnqXEMaOnL7ptGO/+L5kjVks2/JjM5nIZKJAoihLmUyIIAjIGANjTEBEHSIyWUQ6RWSdqm5V1YqIpC3RDImoQETjiGgKM5eIaKOIDKpq4r2Hcw6NRgO1egzvUq3V6l5Hxhuys9OPP7T0lke7tj41nQNiG0FtBmojeBMR2yzIRNhKQh9U6L6kkMGq/7t6Ii8uXoDfvRE2bzprx0n/hc93FLiQi8x1zYq0CdAHvcdkV4V3Dupi9b6OgosR+wRGvU3PPuXSHcXcPiMGnvAvcJIZlwsjG2UzMESUzWa16SExZGxLGFS9sVbFK5SUAGBYWYoIMzN5BbnUgSCaph5xXCfvvSZJouVaw1NWejrfL3NK1a07frHwmpFsXcgRvA3hTRahNeRsHmKaXpZtIDoa0P0AoBb7SwZqEt+/AP/6ZnD5g/LGnHwbvtlZCAYzAYbzJwwo4U5xOl0aUB8jcDHUxUSuoQ4pJE0gmbCt9vFTLm4UM2NHDCxNlidDweiQOAyCUDkwFLBBEFhSZrVEqkDzHLEVAiA6PFBFE0pFkjhS9YjjVJ1Lkfg0sZ3SO+rI8NBSo7vvznuuz8S+lDMhwBbWhmRtVr3JgmwAmAhqAlolij+h5svfqMW4ZKiaFu49F1e/WUz+4MxFJ92GS3MR246M+bYSGEAizD8mJ4d6p+oa8L4OcQnUJzA+hhWnqU+gUdA2cPKxnylNHj/rmOrW9N7+F5JGOiQjyXIYcgC2zRejiVXFw5Np5Y3xMGxgxBMJPMSlFHtPUI1NG/eNmhNm8uODUzZse+nB+x78WVs9KXXaDMgYspyBNyG8iQATwIRZwIawYPOCQj4LICSFDNX9V6qJ5O5bgH/8Q/D4o3JnnfhzfC6yvM/IdvPXADpaLd0KoaJPNS+xmjSF1QYkTeEkVfYpGR8j9Q5WRKvjRkztPf5DC3j0iCkn+AQvlDdUu6rbXaPWn5KrCEEErTwXTTKALbDmRgSaGxNk26bmppoQc7p7ux546PE7ZHvfutHGUJ4DOGMRmEi9sSQcwgYR2GTgOCRvDFXVaJUU81sA9PcM+X92Trru+yT+8w/F4o/O3nbyrTiaGF8cUwgOIMZRreZegerDgB6YJiQSw0uqgYsh3sFrjMB5eE1gfAovHka9pjaM+ke2TxiaNnWujBkzOcxnO/KFXKHNBpnRAODSRm+lVh6q1odqPT0bkjXrnuW+oS3tLo1HsKGADIQDsAnhjEFAFgmHsDYCmYBSG4BMRgMQvQTQcYBOBwBVPN5TStd6hxvuPx9L/xgc3lL6u5N+hpGwuHl0u33a2N/nDiTSXxBIRHWCNMilMdQ7DSVF6h1YUxXvyKhD6h0CCKCCVLxa9YASKYlyK/AOIJAyCUFBDGImB4KlEEoMbywCCtQbQ8QhxFiEJqDYWLDJakBEm4g1UKFPDI/Rq16xY9AdZQzOXzgf/X8sBm85AeM5t8P0eXwtItYRbfZToOavCyDxKj81RCPgaKJ3iL1TAw9xCVgdvHcw6uBVm/pNvQIKpwJV2pkKBQCEFKoMYoKFITVGQQxPBsZYeLIwNoQQw3BAjiNEzNioQKzAebQzkJRW9lXcbXEqctx5uOryYUv1R9LblkP1+JsxjS1+MDJn7wkDuhKEHACQQqD4OUgExJPFq/EpqTglcXDqEXoPJYETDwbgROBVAQY7ABCIJQKYYQBYZogaWGMAMkhhEJiQPLMaG5BTlvWUsgXjvJahAxS1RqpfH6i5eYjxhfs/i7clj+rbm8VXQSf/HB8T4LOj2uwzgaF/0GZ2oeHuVqjq48zIQzHee4QiSLUZgwN4kDYdt0Kkqq38BM1XhYnAMMwKGDQ979y0rERIRbENQJWIPgDorF0m2Ei9Xt0/5N4njH+//zzc9XamRH5H0iAffiOC9gLOVeD8kXl7bxjyxYC+OqMv0VaoPsCEukAigNqg1EEEFlWBQKHUFC9SBoOYiEUhRDoIaInBiSgyBDpJoeN2m9qG2Mv1/SV3iir+s1zFbc9chLc97vgdzWR+uYIfugUnC/C3keUlHQXTaQiX7LUCox9en1XwIBENCqTcvM1FVe0gSAcMzYVgxN6a8IrrBit+IHFyrCF850Orcf/ll781Pfd69K7l0j/mJxhtLb4+ot2uDy3t1T30Vihxeml/2U1WxpVLPol3PA088O7/MwI6/ib819j2YDOb154vvBVSxfXdA+nEBz6Ns4G3T8e9Eb3mUOkdJsW++NT2UjpHVO/V5vrvrRfVh7f3pTNLdZyLdxE84N0HEEtOgMsRzukdcBUV2vRWwYOnbTuG3HZXw4J3wki8Eb2uQ/WdojW/RLz/n+CluKaZTMhzm4eJwB9aFHADFf1X7+X6h/4MG9+LubzrHDhM934KLyhoaSPB3/yx3Nco42+811UPfBbvWvD67vSu/0eb3enEn/K17RkeNExXvPHTvyfxeuVQQ0be9zn50hs//c7Re8aBw3T/Z+TScl3niuBm9cCbLLeXGjr3mA3yl+/1+N9zAEHQ6oA/rxLLBPF49o1Fl54vxVJ08Ge/kwvkN0vvPYAAHv8K6ur8BbVEnlNF6XUArNQS/ziJv2jJ5/Cm07W/k/SeWOE9UddvUJ5+pimpYhODTtyT1Y29fsOrv2fxhXj+vR3t7+l/BQcO0z2fc0ucEyeil+7OfV7xFYXI4gvx4Hs9zl3pPbfCeyA67cfmFiaziVX/BgCUcL1XGf27z/vz8S7vNN6I3t23oN8caW0//+lcF/0PC+4VIBJgZm2aPw3/y8AD/peJ8DAtOQEuZLfAQ0sK7Q0rbv6SE/Yen/L/017ojH8LZ5/xb+Hs93ocr0f/D6s769KBP+5xAAAAAElFTkSuQmCC\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n } else {\\n percent = (percent - 0.5)*2*100;\\n return tinycolor.mix('yellow', 'red', amount = percent).toHexString();\\n }\\n}\",\"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;\",\"strokeWeight\":4,\"strokeOpacity\":0.65,\"color\":\"#1976d3\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var vehicleType = dsData[dsIndex]['vehicleType'];\\r\\nif (typeof vehicleType !== undefined) {\\r\\n if (vehicleType == \\\"bus\\\") {\\r\\n return 'Bus: ${entityName}
Bus route: ${busRoute}
';\\r\\n } else if (vehicleType == \\\"car\\\") {\\r\\n return 'Car: ${entityName}
Current destination: ${destination}
';\\r\\n }\\r\\n}\",\"provider\":\"openstreet-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\"},\"title\":\"Route Map - OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" @@ -90,7 +90,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('tencent-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('tencent-map');\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": "{}\n", "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\":{\"tmDefaultMapType\":\"roadmap\",\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipPattern\":\"
${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details
\",\"markerImageSize\":34,\"tmApiKey\":\"84d6d83e0e51e481e50454ccbe8986b\",\"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', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"useMarkerImageFunction\":true,\"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==\"],\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"mapProviderHere\":\"HERE.normalDay\",\"provider\":\"tencent-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"showPolygon\":false,\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"mapPageSize\":16384},\"title\":\"Tencent Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" @@ -108,7 +108,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('here');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('here');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\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": "{}\n", "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\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"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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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"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}\",\"color\":\"#fe7569\",\"mapProvider\":\"HERE.normalDay\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"provider\":\"here\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"polygonKeyName\":\"coordinates\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"showCoverageOnHover\":true,\"animate\":true,\"maxClusterRadius\":80,\"removeOutsideVisibleBounds\":true,\"zoomOnClick\":true,\"draggableMarker\":false,\"mapProviderHere\":\"HERE.normalDay\",\"credentials\":{\"app_id\":\"AhM6TzD9ThyK78CT3ptx\",\"app_code\":\"p6NPiITB3Vv0GMUFnkLOOg\"},\"mapPageSize\":16384},\"title\":\"HERE Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" @@ -126,7 +126,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('image-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('image-map');\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": "{}\n", "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\":{\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

X Pos: ${xPos:2}
Y Pos: ${yPos:2}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"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,iVBORw0KGgoAAAANSUhEUgAAAB4AAAB/CAYAAAD4mHJdAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAACWAAAAlgB7MGOJQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA3vSURBVGiB7Vt7cFzVef+dc+/d90OrJyO/JSO/4ncxxfULMCYIAyEW08amJJgmM4GmnZjJdNq4gcSGzLQxk3bsaWcaaIHyR8CJrWAbpjgG/AhINsbYxkaSDY6xJFvSrrS7Wu3uvfecr3+cu1pbXhkJs/4nujNndufec77f+d7fd+4uw8gvIxwOfocBaz0e91yXyx0BgKyZiWUz5kcEvBKPJ18EYI+C5rWvkpKSyZGS8LGHGtbQR8ePUUdnB50/f57OfnqWWlpbaN++39O99fdQpCR0NBKJTBwJTfZFE4LBYLmh8+YXXvifKctWrEBPTze9+cbu8/3JVMoWNjwer3/ZsuUTvV4P239gP36yceNZW9CtyWQyei262hcB+7zurU/99Ge3r1nTgJdfevFsqr8/Wlc3rWbGzFkV8+fPr1iwYEEJgLadO3cmbr/jjohh6KXHPjxamsmar39pjoPBYHl5aUnnqZY2/b1Dh9LdPd39kUgk6PP5PD6fH36/Dz6fDx6PF+fOfdZ9+pPTgbq6Ou+aBx+0k/0DVYlEIjYcbX4tYM5pxeK/WKIDwM7Gxt0TJox/dtLESXC53JuHzvV4PBVHDjfvAYDZs+fonMsV16R9rYeM8XG1tbUAgMrKsrDP659DRJ5gMNhbaH5NTU0IAMaPHw9IPv5LAxORy+31AgBcLsO41lwAcLu9BgAYLheIkftLAxfzGgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4DHgMeAx4D/lME1ke7gDF8ltbOHe3W923oEwYi1jxftWfZWgAziwacZkd2pfyN96XN5IIu7dMtIKA9/TI+zqCnFps2Alg5UlojFnVqIHZUlO2sl4RyC4CU+SEEylux8Z/iyc7mrxw4U7UnYwvGpXMYKIgNGdwXC/76C48oRw3sDWfnCgIkARJXcpwbvpA1e6T0Rq5jDr8EAHKA6OpjUOJwfeXAJAEhAXAGgEPKq+dIMVJqowDO4RAAC0rHV21u5LijAJaABAOIAY5Oh15iFMgj1zEpcUuuXjpIWeCouxjAtnIZcGKA5AVFbRfazPUC50QrKe8+Qy8qiqjBYIODA5DgBd1pBO9WRg9sy7yOhXBca+icYrgTOUGOiKnIVdCdisAxJGBTPsYW0nHRrJqgfNmGVtiqaeR1xchF7Vgz40q/BUNmISlcL7CUgJAMnOUiVwEdF0PURIAAVHaC8ucbAiwcQAb1KQpwXMjFrhtYMcOVO8lhOB457ujcKZd9hBguSYwcelTupKyaQWKYJFEU4xJw/Dhfcw29ilSBcNjEoTucFnSnkeOOvvTJpcVC1cYoGB5NAGEQTukjMAzHoghJghyWCRjenYoTuZjKx8xJiwU4LrSZ6waWpIoBjTuRqxDHRUkSUMWAJAZp6QU5FqOw65HHapG3bGVcBTZXDI5VnFaFgBL1yC34uoBJqEJeIwD2MMY1ilZidAFEMlDOqm9UdpJ0ZawumI+LU9ArwhyqWxyNz14XsBAMUnLVH0ttGB0XococdCGWE3XhOV85MF1WV2OY3omK0S2SkxgYAZYYJoAUpcqEEjG/Ru80isA1ysMXYNCnCum4aKUPgTu90w3sFinXL6nO/MadCAhiKloxBjFMeSuK0S1Kylv1cE1bUVoYyHwhoI6bCswpjjuxK5u2G2lcti2jzNCRTluioHEVw52EBA5/2LKsLBL+h2gs/o+Fjpa+MqtmjCbkqQJSYFF3T3zRsPMvA75i7UiBA4FApa6z5+fNnbd6/frHADghk7QdlhAHdMY0KXkZAHAuozaRMDRtKYMdAYDVq1fjcHPTD860nZlsS3qsv7+/+6pNDr0RDAanGTrf85Onnq75/uNPIJ1O4+dbnj34Ot6B4eFLqksqUeEvgcflAREhZabR09+Li/EorLQ4eFv317D2oW8t0XUdu3a9jud/9auztqD6ZDLZOixwOByeouv8D1u3brtpxYrb0XS4Kfbj3//8VHC8d0nDLXfj67OWIeQJgDGADfoOAxHQl05i14l92PHBXiTPp/c/OrFh9vwF8yMnjp/A5s2bOqXEbX19fX+8CriqqspvmunDTz/10xkr71qFnY07Tr1i7aqsLg2Vb6h/GOPCpdAYgTPlNLmF5AzpvBRp74viX3a/hO6+ge47+hZG61fVTz9y+DCee27Lx15fYFFHR8cAcNkPuw2DPXfP1+vvvf+BB7Br967WX9Mbk70eCn33zlWoCrsgKAFBCdgy/2nLBCyZgCUSMGUSpkzC0G1MrKzE0XMt/la9I0QnM+cWL15cmkwmK1tOnwpksuabg8YVifjnhEOlj69dtw6nT51Kv2q96fYG4fG7gbJwFhn7cxicIJgEZwAfEiokGASpWG1KhvIwg1/91ti1N9DEJ7ZOzKxdt87T1Nz8A67jv2Kx/o85AJDk//zXjzzCAeA/D7zU6PZjkkuXcBuEjN2OrGiHabfDFB2w7HZYoh3mVaMDWWdu1m6Hy5Bw6RIuP6b87+HXdgDAww8/zIXgGwFADwQCFYFA4BuLFi3CoUN/6LRmyL/y6gSXTtC4QDTVgQo/B5iEJFJ6Rt64lI6Vfi3JYBFHd1JA5wIunUNIQvpr/C+bm5u65s9fWBnwe9dISWVc0/DNhQsX6gDwTuuhd3WNYOSGTjjSehGp7EVYsguWuJQfssu51wVTXIIpLsGWlzBgXsSRM5dg6Hk6uk787Zb39gHA7NlzDM7xoM4Yli5fvgJSSiRmmbP9HNA0Qm4D6axEc6uJ6eOzuCloQuOOjlneqiUx2BK4lDBwut2DTFaHoXFYGilaHEjMMOdKKXHb4tvw/nvvL9UZ+Lyb6+pw/PjxpOZhsziX0DigcYLG1QaEBD69ZKA7wRHx2/C7BDSNwEi9AEmZGmJJA/1Z9SJM12hwvcYBzgmaj89obW3pr62dGmCcz+cuQ68GgEtdl7oYU40CZwSeW+As1rmy5KzNkbY1WILDlOp71ubgnKA7czVO4NyhwQhcFS7o6urq5pzDMLRqnXEtCACpdCrFHOHlAsTgYEq0nCnj0jnBY6i8KCTLBxbmzB2yPkczmU4lAYAxHtKFECYAPeDzBQZD4GU+motMueXklECWc7QkSaVDGoTAVetz8AGfLwQAQoisbtt2N4BJZaVlpZQjkntdS8w5UFOFni0YLMGhWfny1rbVPVuoOVKyK9ZeTrMsUl7qAHdzkPyktzeG2tqbw8KihCQlPjVUl2hLBkswmDZD1mJIWxwDWTXSFkfWUs8sZ64QzlqHjiRA2tQ7ZcqUYCwWgyT6hBNjb+3ZvQehUIi52tje3M6FyHHIYNkOqM2RsTjS2cuAs+pe1uYKPLcBkduA+m60sH1+v5/t3fsWGGP/x6VkjR98cAQAMNc7bXJepAyWzWHaimjW4siYDGmTY8DkGMhqapgcaVM9yw5ugMOyeX4DkmGub1otABz/6DiI2O94IpE4E+3p+aCzsxP333PfAvOi2G8JBtMRbU68GZMj44Ao0BzXmgOsRk7spq1oWILB6rQP3nt3/byLnZ2IxWKH4/H4pxoAeFzuC21tretW3rUKnk5mtWiflzAGxhgDQ66IYyrnOnqzBFfDZjAdLk1HMnkpMWRNLldmFomamtrIL/71F+iPJ/8mnc2e4QDQm0jsOXfu3L6TJ0/ivtX3T607M26P6SzMWI5eB7ktPHLPc/MV5xwTjpe9sfLOu2pOHD+JCxc+fyeWSLyZdzCoWsvjNpqef/6F8KTJU/DDLT/a3jM90eDWCS5dqmDvxF7NCRSAOikQhCuMUXHMEDjm3v7jb/+oIRrtxpMbnuzNmvatiUSi7QpgAAiFQneXlZbs3rGjUauorMSmLc+8dShy7HbDELqeA3bC4GCScHxWSMDOgVuaPb2t+t3vPfK9O1P9A/j7v3vC7ov318fj8bdyWFf8YCSbzZ7VNHb+tVdfrV911ypt/bcfq52J2uTBg+//LhWwZ0nJYTtWf6WrcccDGFgLdn5nwkPVD9Q/MLOzsxNPbvhhNpUc+G5vPL7jcqxBjonozwEsBzD5lVde9jy5YcPqTZufKX90/WOwbRv7330nsffDt08dSB41EkZyHPfwmwBAZuTFsBm48GeuWfai2oUzp02fFjKzJhp3NuLFF/+765e//Pfd31q71gLwGYC3GWNNAMCIaBKAJwBUO3uQnZ2d/MyZNv1vn/j+LUuXLq/Z/MyzCIfDTmxW8Y+IVFyWqjKRQkDYNqKxGDb97GkcOXLk7LZt/9F8c12dqKqqYM4LYALQCWAbI6J/A1AGgKK9vSBhoa8vEe+N9TwejcZYU1MTfrN9O6puqkJDw0NYtnwFpk6dCsZUMrFtG22trTiw/11s3/4aotEo1jQ04NZFt6KsrJTCoZKtJaWRiGG4KBKJ5BJWnw4gDedAx+0yMJCywLnQGWOSMabV1NbikUfX40J7B367sxFbt25DMhGHZZkgAC7DhWAojOpx4zF3wS0YP64aVZUVYCoQSN2la4bhIsNlcOS73H5GRBUAHgcwBYABAD09PZROp1gq2V8WTybq4vH4xEQ8oSWSSfSnUkinM7As9RdUw9Dh9XoR8PsQCgYRCodESTj0x1Aw2OrxBXsDgYBdXl6eM2IB4CyAbZcb12wASwBMB1Dq7C4ACJZIJHstM5PWdC2TTmcom80wEtySAFwupum6wbxeDxeCuT0et8/v94UBTTrSJABRAKcAHGCMnbrKjy/bRBjAHAATAFQ5NuAF4IFqAtyOKzKo83MLgAkgA2AAQB+ADgCfAzjBGIsPxfh/6wbDK7xbMFYAAAAASUVORK5CYII=\",\"data:image/png;base64,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\",\"data:image/png;base64,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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"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}\",\"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,\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"provider\":\"image-map\",\"showTooltipAction\":\"click\",\"mapPageSize\":16384},\"title\":\"Image Map\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" @@ -144,7 +144,7 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('google-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('google-map');\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": "{}\n", "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\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"gmDefaultMapType\":\"roadmap\",\"gmApiKey\":\"AIzaSyDoEx2kaGz3PxwbI9T7ccTSg5xjdw8Nw8Q\",\"useColorFunction\":true,\"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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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"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}\",\"color\":\"#fe7568\",\"showTooltip\":true,\"autocloseTooltip\":true,\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"provider\":\"google-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"mapPageSize\":16384,\"useLabelFunction\":false,\"useTooltipFunction\":false},\"title\":\"Google Maps\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" @@ -162,11 +162,11 @@ "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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\n}\n\nself.actionSources = function() {\n return TbMapWidgetV2.actionSources();\n}\n\nself.onDestroy = function() {\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\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.getSettingsSchema = function() {\n return TbMapWidgetV2.settingsSchema('openstreet-map');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbMapWidgetV2.dataKeySettingsSchema('openstreet-map');\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": "{}\n", "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\":{\"fitMapBounds\":true,\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"tooltipPattern\":\"${entityName}

Latitude: ${latitude:7}
Longitude: ${longitude:7}
Temperature: ${temperature} °C
See advanced settings for details\",\"markerImageSize\":34,\"useColorFunction\":true,\"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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\"],\"useMarkerImageFunction\":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', amount = percent).toHexString();\\n\\t}\\n\\treturn 'blue';\\n}\\n\",\"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}\",\"color\":\"#fe7569\",\"mapProvider\":\"OpenStreetMap.Mapnik\",\"showTooltip\":true,\"autocloseTooltip\":true,\"tooltipFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}
Energy: ${energy:2} kWt
';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}
Temperature: ${temperature:2} °C
';\\r\\n }\\r\\n}\",\"labelFunction\":\"var deviceType = dsData[dsIndex]['Type'];\\r\\nif (typeof deviceType !== undefined) {\\r\\n if (deviceType == \\\"energy meter\\\") {\\r\\n return '${entityName}, ${energy:2} kWt';\\r\\n } else if (deviceType == \\\"thermometer\\\") {\\r\\n return '${entityName}, ${temperature:2} °C';\\r\\n }\\r\\n}\",\"provider\":\"openstreet-map\",\"defaultCenterPosition\":\"0,0\",\"showTooltipAction\":\"click\",\"mapPageSize\":16384,\"useTooltipFunction\":false},\"title\":\"OpenStreetMap\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } } ] -} \ No newline at end of file +} 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 cf9537fb83..6eaddc768a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -59,7 +59,8 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; +import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; +import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; @@ -85,7 +86,6 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.rpc.TbRpcService; import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService; -import org.thingsboard.server.service.script.JsExecutorService; import org.thingsboard.server.service.script.JsInvokeService; import org.thingsboard.server.service.session.DeviceSessionCacheService; import org.thingsboard.server.service.sms.SmsExecutorService; @@ -421,7 +421,11 @@ public class ActorSystemContext { @Autowired(required = false) @Getter - private CassandraBufferedRateExecutor cassandraBufferedRateExecutor; + private CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; + + @Autowired(required = false) + @Getter + private CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; @Autowired(required = false) @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index d16c1f10fd..a12887db81 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -244,7 +244,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { rpc.setExpirationTime(request.getExpirationTime()); rpc.setRequest(JacksonUtil.valueToTree(request)); rpc.setStatus(status); - rpc.setAdditionalInfo(JacksonUtil.valueToTree(request.getAdditionalInfo())); + rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo())); return systemContext.getTbRpcService().save(tenantId, rpc); } @@ -581,7 +581,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { systemContext.getTbRpcService().save(tenantId, new RpcId(requestMd.getMsg().getMsg().getId()), status, response); } } finally { - if (hasError) { + if (hasError && !requestMd.isDelivered()) { sendNextPendingRequest(context); } } @@ -731,6 +731,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { } private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) { + log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId [{}] sessionMd [{}]", sessionId, sessionMd); notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!"); } 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 78dbadc9bf..e3896b87a9 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 @@ -190,13 +190,13 @@ class DefaultTbContext implements TbContext { @Override public void enqueueForTellNext(TbMsg tbMsg, String queueName, String relationType, Runnable onSuccess, Consumer onFailure) { TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); - enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure); + enqueueForTellNext(tpi, queueName, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure); } @Override public void enqueueForTellNext(TbMsg tbMsg, String queueName, Set relationTypes, Runnable onSuccess, Consumer onFailure) { TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName); - enqueueForTellNext(tpi, tbMsg, relationTypes, null, onSuccess, onFailure); + enqueueForTellNext(tpi, queueName, tbMsg, relationTypes, null, onSuccess, onFailure); } private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) { @@ -211,9 +211,13 @@ class DefaultTbContext implements TbContext { } private void enqueueForTellNext(TopicPartitionInfo tpi, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { + enqueueForTellNext(tpi, source.getQueueName(), source, relationTypes, failureMessage, onSuccess, onFailure); + } + + private void enqueueForTellNext(TopicPartitionInfo tpi, String queueName, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) { RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId(); RuleNodeId ruleNodeId = nodeCtx.getSelf().getId(); - TbMsg tbMsg = TbMsg.newMsg(source, ruleChainId, ruleNodeId); + TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId); TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits()) @@ -553,8 +557,13 @@ class DefaultTbContext implements TbContext { } @Override - public TbResultSetFuture submitCassandraTask(CassandraStatementTask task) { - return mainCtx.getCassandraBufferedRateExecutor().submit(task); + public TbResultSetFuture submitCassandraReadTask(CassandraStatementTask task) { + return mainCtx.getCassandraBufferedRateReadExecutor().submit(task); + } + + @Override + public TbResultSetFuture submitCassandraWriteTask(CassandraStatementTask task) { + return mainCtx.getCassandraBufferedRateWriteExecutor().submit(task); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java index 98294b241a..5c40461abc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java @@ -25,7 +25,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -34,10 +33,11 @@ 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.id.UUIDBased; +import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody; +import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.service.rpc.LocalRequestMetaData; import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService; import org.thingsboard.server.service.security.AccessValidator; @@ -60,7 +60,7 @@ public abstract class AbstractRpcController extends BaseController { protected TbCoreDeviceRpcService deviceRpcService; @Autowired - private AccessValidator accessValidator; + protected AccessValidator accessValidator; @Value("${server.rest.server_side_rpc.min_timeout:5000}") protected long minTimeout; diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index d8406088c7..d9a8f436b9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -39,6 +41,8 @@ import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.update.UpdateService; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api/admin") @@ -59,10 +63,14 @@ public class AdminController extends BaseController { @Autowired private UpdateService updateService; + @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", + notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) @ResponseBody - public AdminSettings getAdminSettings(@PathVariable("key") String key) throws ThingsboardException { + public AdminSettings getAdminSettings( + @ApiParam(value = "A string value of the key (e.g. 'general' or 'mail').") + @PathVariable("key") String key) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key)); @@ -75,10 +83,17 @@ public class AdminController extends BaseController { } } + + @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", + notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " + + "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + + "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings", method = RequestMethod.POST) @ResponseBody - public AdminSettings saveAdminSettings(@RequestBody AdminSettings adminSettings) throws ThingsboardException { + public AdminSettings saveAdminSettings( + @ApiParam(value = "A JSON value representing the Administration Settings.") + @RequestBody AdminSettings adminSettings) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); @@ -94,6 +109,8 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Get the Security Settings object", + notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/securitySettings", method = RequestMethod.GET) @ResponseBody @@ -106,10 +123,14 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Update Security Settings (saveSecuritySettings)", + notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/securitySettings", method = RequestMethod.POST) @ResponseBody - public SecuritySettings saveSecuritySettings(@RequestBody SecuritySettings securitySettings) throws ThingsboardException { + public SecuritySettings saveSecuritySettings( + @ApiParam(value = "A JSON value representing the Security Settings.") + @RequestBody SecuritySettings securitySettings) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE); securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings)); @@ -119,14 +140,19 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Send test email (sendTestMail)", + notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " + + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST) - public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException { + public void sendTestMail( + @ApiParam(value = "A JSON value representing the Mail Settings.") + @RequestBody AdminSettings adminSettings) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); adminSettings = checkNotNull(adminSettings); if (adminSettings.getKey().equals("mail")) { - if(!adminSettings.getJsonValue().has("password")) { + if (!adminSettings.getJsonValue().has("password")) { AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); } @@ -138,9 +164,14 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Send test sms (sendTestMail)", + notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " + + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) - public void sendTestSms(@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { + public void sendTestSms( + @ApiParam(value = "A JSON value representing the Test SMS request.") + @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException { try { accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); smsService.sendTestSms(testSmsRequest); @@ -149,6 +180,9 @@ public class AdminController extends BaseController { } } + @ApiOperation(value = "Check for new Platform Releases (checkUpdates)", + notes = "Check notifications about new platform releases. " + + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/updates", method = RequestMethod.GET) @ResponseBody 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 7bbc84fb32..d376f5984f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -15,8 +15,11 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -49,17 +52,46 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +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.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api") public class AlarmController extends BaseController { public static final String ALARM_ID = "alarmId"; + private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; + private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value"; + private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; + private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; + private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; + private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on of next alarm fields: type, severity or status"; + private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; + private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; + private static final String ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION = "A boolean value to specify if the alarm originator name will be " + + "filled in the AlarmInfo object field: 'originatorName' or will returns as null."; - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Get Alarm (getAlarmById)", + notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET) @ResponseBody - public Alarm getAlarmById(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { + public Alarm getAlarmById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -69,10 +101,14 @@ public class AlarmController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Get Alarm Info (getAlarmInfoById)", + notes = "Fetch the Alarm Info object based on the provided Alarm Id. " + + ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET) @ResponseBody - public AlarmInfo getAlarmInfoById(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { + public AlarmInfo getAlarmInfoById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -82,10 +118,20 @@ public class AlarmController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Create or update Alarm (saveAlarm)", + notes = "Creates or Updates the Alarm. " + + "When creating alarm, platform generates Alarm Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " + + "Referencing non-existing Alarm Id will cause 'Not Found' error. " + + "\n\nPlatform also deduplicate the alarms based on the entity id of originator and alarm 'type'. " + + "For example, if the user or system component create the alarm with the type 'HighTemperature' for device 'Device A' the new active alarm is created. " + + "If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " + + "If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + , produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm", method = RequestMethod.POST) @ResponseBody - public Alarm saveAlarm(@RequestBody Alarm alarm) throws ThingsboardException { + public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { try { alarm.setTenantId(getCurrentUser().getTenantId()); @@ -106,10 +152,12 @@ public class AlarmController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Delete Alarm (deleteAlarm)", + notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE) @ResponseBody - public Boolean deleteAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { + public Boolean deleteAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -124,15 +172,19 @@ public class AlarmController extends BaseController { sendAlarmDeleteNotificationMsg(getTenantId(), alarmId, relatedEdgeIds, alarm); return alarmService.deleteAlarm(getTenantId(), alarmId); - } catch (Exception e) { + } catch (Exception e) { throw handleException(e); } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Acknowledge Alarm (ackAlarm)", + notes = "Acknowledge the Alarm. " + + "Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void ackAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { + public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -149,10 +201,14 @@ public class AlarmController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Clear Alarm (clearAlarm)", + notes = "Clear the Alarm. " + + "Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void clearAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { + public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); try { AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); @@ -169,21 +225,36 @@ public class AlarmController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Get Alarms (getAlarms)", + notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody public PageData getAlarms( - @PathVariable("entityType") String strEntityType, - @PathVariable("entityId") String strEntityId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, + @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @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_PROPERTY_DESCRIPTION, allowableValues = ALARM_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 = ALARM_QUERY_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime, + @ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @RequestParam(required = false) Boolean fetchOriginator ) throws ThingsboardException { checkParameter("EntityId", strEntityId); @@ -204,20 +275,35 @@ public class AlarmController extends BaseController { throw handleException(e); } } - - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Get All Alarms (getAllAlarms)", + notes = "Returns a page of alarms that belongs to the current user owner. " + + "If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " + + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarms", method = RequestMethod.GET) @ResponseBody public PageData getAllAlarms( + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, + @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @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_PROPERTY_DESCRIPTION, allowableValues = ALARM_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 = ALARM_QUERY_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime, + @ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @RequestParam(required = false) Boolean fetchOriginator ) throws ThingsboardException { AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); @@ -239,13 +325,21 @@ public class AlarmController extends BaseController { } } - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + , produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody public AlarmSeverity getHighestAlarmSeverity( - @PathVariable("entityType") String strEntityType, - @PathVariable("entityId") String strEntityId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, + @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status ) throws ThingsboardException { checkParameter("EntityId", strEntityId); diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index d63674b7ee..f7098868d8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -16,9 +16,12 @@ package org.thingsboard.server.controller; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -37,8 +40,8 @@ import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetSearchQuery; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; 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.AssetId; import org.thingsboard.server.common.data.id.CustomerId; @@ -61,9 +64,28 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; - +import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_NAME_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_TYPE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; +import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; @RestController @TbCoreComponent @@ -75,10 +97,16 @@ public class AssetController extends BaseController { public static final String ASSET_ID = "assetId"; + @ApiOperation(value = "Get Asset (getAssetById)", + notes = "Fetch the Asset object based on the provided Asset Id. " + + "If the user has the authority of 'Tenant Administrator', the server checks that the asset is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the asset is assigned to the same customer." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + , produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.GET) @ResponseBody - public Asset getAssetById(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public Asset getAssetById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); try { AssetId assetId = new AssetId(toUUID(strAssetId)); @@ -88,10 +116,16 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Asset Info (getAssetInfoById)", + notes = "Fetch the Asset Info object based on the provided Asset Id. " + + "If the user has the authority of 'Tenant Administrator', the server checks that the asset is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the asset is assigned to the same customer. " + + ASSET_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset/info/{assetId}", method = RequestMethod.GET) @ResponseBody - public AssetInfo getAssetInfoById(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public AssetInfo getAssetInfoById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); try { AssetId assetId = new AssetId(toUUID(strAssetId)); @@ -101,10 +135,15 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Create Or Update Asset (saveAsset)", + notes = "Creates or Updates the Asset. When creating asset, platform generates Asset Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address) " + + "The newly created Asset id will be present in the response. " + + "Specify existing Asset id to update the asset. " + + "Referencing non-existing Asset Id will cause 'Not Found' error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/asset", method = RequestMethod.POST) @ResponseBody - public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException { + public Asset saveAsset(@ApiParam(value = "A JSON value representing the asset.") @RequestBody Asset asset) throws ThingsboardException { try { if (TB_SERVICE_QUEUE.equals(asset.getType())) { throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); @@ -116,7 +155,7 @@ public class AssetController extends BaseController { Asset savedAsset = checkNotNull(assetService.saveAsset(asset)); - onAssetCreatedOrUpdated(savedAsset, asset.getId() != null); + onAssetCreatedOrUpdated(savedAsset, asset.getId() != null, getCurrentUser()); return savedAsset; } catch (Exception e) { @@ -126,9 +165,9 @@ public class AssetController extends BaseController { } } - private void onAssetCreatedOrUpdated(Asset asset, boolean updated) { + private void onAssetCreatedOrUpdated(Asset asset, boolean updated, SecurityUser user) { try { - logEntityAction(asset.getId(), asset, + logEntityAction(user, asset.getId(), asset, asset.getCustomerId(), updated ? ActionType.UPDATED : ActionType.ADDED, null); } catch (ThingsboardException e) { @@ -140,10 +179,12 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Delete asset (deleteAsset)", + notes = "Deletes the asset and all the relations (from and to the asset). Referencing non-existing asset Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteAsset(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public void deleteAsset(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); try { AssetId assetId = new AssetId(toUUID(strAssetId)); @@ -167,11 +208,13 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Assign asset to customer (assignAssetToCustomer)", + notes = "Creates assignment of the asset to customer. Customer will be able to query asset afterwards." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/asset/{assetId}", method = RequestMethod.POST) @ResponseBody - public Asset assignAssetToCustomer(@PathVariable("customerId") String strCustomerId, - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public Asset assignAssetToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(ASSET_ID, strAssetId); try { @@ -201,10 +244,12 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Unassign asset from customer (unassignAssetFromCustomer)", + notes = "Clears assignment of the asset to customer. Customer will not be able to query asset afterwards." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/asset/{assetId}", method = RequestMethod.DELETE) @ResponseBody - public Asset unassignAssetFromCustomer(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public Asset unassignAssetFromCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); try { AssetId assetId = new AssetId(toUUID(strAssetId)); @@ -235,10 +280,14 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Make asset publicly available (assignAssetToPublicCustomer)", + notes = "Asset will be available for non-authorized (not logged-in) users. " + + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " + + "However, users that are logged-in and belong to different tenant will not be able to access the asset." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/asset/{assetId}", method = RequestMethod.POST) @ResponseBody - public Asset assignAssetToPublicCustomer(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public Asset assignAssetToPublicCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(ASSET_ID, strAssetId); try { AssetId assetId = new AssetId(toUUID(strAssetId)); @@ -261,15 +310,24 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Tenant Assets (getTenantAssets)", + notes = "Returns a page of assets owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantAssets( + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -284,15 +342,24 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Tenant Asset Infos (getTenantAssetInfos)", + notes = "Returns a page of assets info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantAssetInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -307,10 +374,14 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Tenant Asset (getTenantAsset)", + notes = "Requested asset must be owned by tenant that the user belongs to. " + + "Asset name is an unique property of asset. So it can be used to identify the asset." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET) @ResponseBody public Asset getTenantAsset( + @ApiParam(value = ASSET_NAME_DESCRIPTION) @RequestParam String assetName) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -320,16 +391,26 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Customer Assets (getCustomerAssets)", + notes = "Returns a page of assets objects assigned to customer. " + + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerAssets( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -347,16 +428,26 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Customer Asset Infos (getCustomerAssetInfos)", + notes = "Returns a page of assets info objects assigned to customer. " + + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerAssetInfos( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -374,10 +465,13 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Assets By Ids (getAssetsByIds)", + notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ", produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET) @ResponseBody public List getAssetsByIds( + @ApiParam(value = "A list of assets ids, separated by comma ','") @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException { checkArrayParameter("assetIds", strAssetIds); try { @@ -400,6 +494,10 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Find related assets (findByQuery)", + notes = "Returns all assets that are related to the specific entity. " + + "The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " + + "See 'Model' tab of the Parameters for more info.", produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/assets", method = RequestMethod.POST) @ResponseBody @@ -424,6 +522,8 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get Asset Types (getAssetTypes)", + notes = "Returns a set of unique asset types based on assets 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 = "/asset/types", method = RequestMethod.GET) @ResponseBody @@ -438,11 +538,18 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Assign asset to edge (assignAssetToEdge)", + notes = "Creates assignment of an existing asset to an instance of The Edge. " + + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive a copy of assignment asset " + + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once asset will be delivered to edge service, it's going to be available for usage on remote edge instance.", + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST) @ResponseBody - public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId, - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public Asset assignAssetToEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(ASSET_ID, strAssetId); try { @@ -471,11 +578,18 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Unassign asset from edge (unassignAssetFromEdge)", + notes = "Clears assignment of the asset to the edge. " + + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive an 'unassign' command to remove asset " + + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once 'unassign' command will be delivered to edge service, it's going to remove asset locally.", + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.DELETE) @ResponseBody - public Asset unassignAssetFromEdge(@PathVariable(EDGE_ID) String strEdgeId, - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { + public Asset unassignAssetFromEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(ASSET_ID, strAssetId); try { @@ -504,18 +618,30 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Get assets assigned to edge (getEdgeAssets)", + notes = "Returns a page of assets assigned to edge. " + + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getEdgeAssets( + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_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 = "Timestamp. Assets with creation time before it won't be queried") @RequestParam(required = false) Long startTime, + @ApiParam(value = "Timestamp. Assets with creation time after it won't be queried") @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { @@ -547,11 +673,14 @@ public class AssetController extends BaseController { } } + @ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)", + notes = "There's an ability to import the bulk of assets using the only .csv file.", produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PostMapping("/asset/bulk_import") public BulkImportResult processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception { - return assetBulkImportService.processBulkImport(request, getCurrentUser(), importedAssetInfo -> { - onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated()); + SecurityUser user = getCurrentUser(); + return assetBulkImportService.processBulkImport(request, user, importedAssetInfo -> { + onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated(), user); }); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index b9d9c6ba15..9222f66acd 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.apache.commons.lang3.StringUtils; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,23 +42,60 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.AUDIT_LOG_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; + @RestController @TbCoreComponent @RequestMapping("/api") public class AuditLogController extends BaseController { + private static final String AUDIT_LOG_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the AuditLog class field: 'createdTime'."; + private static final String AUDIT_LOG_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the AuditLog class field: 'createdTime'."; + private static final String AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION = "A String value representing comma-separated list of action types. " + + "This parameter is optional, but it can be used to filter results to fetch only audit logs of specific action types. " + + "For example, 'LOGIN', 'LOGOUT'. See the 'Model' tab of the Response Class for more details."; + private static final String AUDIT_LOG_SORT_PROPERTY_DESCRIPTION = "Property of audit log to sort by. " + + "See the 'Model' tab of the Response Class for more details. " + + "Note: entityType sort property is not defined in the AuditLog class, however, it can be used to sort audit logs by types of entities that were logged."; + + + @ApiOperation(value = "Get audit logs by customer id (getAuditLogsByCustomerId)", + notes = "Returns a page of audit logs related to the targeted customer entities (devices, assets, etc.), " + + "and users actions (login, logout, etc.) that belong to this customer. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getAuditLogsByCustomerId( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_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 = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime, + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("CustomerId", strCustomerId); @@ -68,18 +108,32 @@ public class AuditLogController extends BaseController { } } + @ApiOperation(value = "Get audit logs by user id (getAuditLogsByUserId)", + notes = "Returns a page of audit logs related to the actions of targeted user. " + + "For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getAuditLogsByUserId( + @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable("userId") String strUserId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_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 = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime, + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("UserId", strUserId); @@ -92,19 +146,35 @@ public class AuditLogController extends BaseController { } } + @ApiOperation(value = "Get audit logs by entity id (getAuditLogsByEntityId)", + notes = "Returns a page of audit logs related to the actions on the targeted entity. " + + "Basically, this API call is used to get the full lifecycle of some specific entity. " + + "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getAuditLogsByEntityId( + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String strEntityId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_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 = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime, + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { checkParameter("EntityId", strEntityId); @@ -118,17 +188,29 @@ public class AuditLogController extends BaseController { } } + @ApiOperation(value = "Get all audit logs (getAuditLogs)", + notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getAuditLogs( + @ApiParam(value = PAGE_SIZE_DESCRIPTION) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION) @RequestParam int page, + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_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 = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime, + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION) @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); 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 3c4db474ef..e7d553834b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; @@ -50,6 +50,11 @@ import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; 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; +import org.thingsboard.server.service.security.model.JwtTokenPair; +import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest; +import org.thingsboard.server.service.security.model.ResetPasswordRequest; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -74,9 +79,13 @@ public class AuthController extends BaseController { private final AuditLogService auditLogService; private final ApplicationEventPublisher eventPublisher; + + @ApiOperation(value = "Get current User (getUser)", + notes = "Get the information about the User which credentials are used to perform this REST API call.") @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/user", method = RequestMethod.GET) - public @ResponseBody User getUser() throws ThingsboardException { + public @ResponseBody + User getUser() throws ThingsboardException { try { SecurityUser securityUser = getCurrentUser(); return userService.findUserById(securityUser.getTenantId(), securityUser.getId()); @@ -85,6 +94,8 @@ public class AuthController extends BaseController { } } + @ApiOperation(value = "Logout (logout)", + notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ") @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/logout", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @@ -92,13 +103,17 @@ public class AuthController extends BaseController { logLogoutAction(request); } + @ApiOperation(value = "Change password for current User (changePassword)", + notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.") @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public ObjectNode changePassword(@RequestBody JsonNode changePasswordRequest) throws ThingsboardException { + public ObjectNode changePassword( + @ApiParam(value = "Change Password Request") + @RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException { try { - String currentPassword = changePasswordRequest.get("currentPassword").asText(); - String newPassword = changePasswordRequest.get("newPassword").asText(); + String currentPassword = changePasswordRequest.getCurrentPassword(); + String newPassword = changePasswordRequest.getNewPassword(); SecurityUser securityUser = getCurrentUser(); UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, securityUser.getId()); if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { @@ -123,6 +138,8 @@ public class AuthController extends BaseController { } } + @ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)", + notes = "API call to get the password policy for the password validation form(s).") @RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET) @ResponseBody public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException { @@ -135,8 +152,13 @@ public class AuthController extends BaseController { } } + @ApiOperation(value = "Check Activate User Token (checkActivateToken)", + notes = "Checks the activation token and forwards user to 'Create Password' page. " + + "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " + + "If token is not valid, returns '409 Conflict'.") @RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET) public ResponseEntity checkActivateToken( + @ApiParam(value = "The activate token string.") @RequestParam(value = "activateToken") String activateToken) { HttpHeaders headers = new HttpHeaders(); HttpStatus responseStatus; @@ -157,13 +179,17 @@ public class AuthController extends BaseController { return new ResponseEntity<>(headers, responseStatus); } + @ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)", + notes = "Request to send the reset password email if the user with specified email address is present in the database. " + + "Always return '200 OK' status for security purposes.") @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) public void requestResetPasswordByEmail( - @RequestBody JsonNode resetPasswordByEmailRequest, + @ApiParam(value = "The JSON object representing the reset password email request.") + @RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest, HttpServletRequest request) throws ThingsboardException { try { - String email = resetPasswordByEmailRequest.get("email").asText(); + String email = resetPasswordByEmailRequest.getEmail(); UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email); User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId()); String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request); @@ -176,8 +202,13 @@ public class AuthController extends BaseController { } } + @ApiOperation(value = "Check password reset token (checkResetToken)", + notes = "Checks the password reset token and forwards user to 'Reset Password' page. " + + "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " + + "If token is not valid, returns '409 Conflict'.") @RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET) public ResponseEntity checkResetToken( + @ApiParam(value = "The reset token string.") @RequestParam(value = "resetToken") String resetToken) { HttpHeaders headers = new HttpHeaders(); HttpStatus responseStatus; @@ -198,16 +229,24 @@ public class AuthController extends BaseController { return new ResponseEntity<>(headers, responseStatus); } + @ApiOperation(value = "Activate User", + notes = "Checks the activation token and updates corresponding user password in the database. " + + "Now the user may start using his password to login. " + + "The response already contains the [JWT](https://jwt.io) activation and refresh tokens, " + + "to simplify the user activation flow and avoid asking user to input password again after activation. " + + "If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " + + "If token is not valid, returns '404 Bad Request'.") @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @ResponseBody - public JsonNode activateUser( - @RequestBody JsonNode activateRequest, + public JwtTokenPair activateUser( + @ApiParam(value = "Activate user request.") + @RequestBody ActivateUserRequest activateRequest, @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException { try { - String activateToken = activateRequest.get("activateToken").asText(); - String password = activateRequest.get("password").asText(); + String activateToken = activateRequest.getActivateToken(); + String password = activateRequest.getPassword(); systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null); String encodedPassword = passwordEncoder.encode(password); UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword); @@ -232,25 +271,26 @@ public class AuthController extends BaseController { JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - ObjectMapper objectMapper = new ObjectMapper(); - ObjectNode tokenObject = objectMapper.createObjectNode(); - tokenObject.put("token", accessToken.getToken()); - tokenObject.put("refreshToken", refreshToken.getToken()); - return tokenObject; + return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); } catch (Exception e) { throw handleException(e); } } + @ApiOperation(value = "Reset password (resetPassword)", + notes = "Checks the password reset token and updates the password. " + + "If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " + + "If token is not valid, returns '404 Bad Request'.") @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @ResponseBody - public JsonNode resetPassword( - @RequestBody JsonNode resetPasswordRequest, + public JwtTokenPair resetPassword( + @ApiParam(value = "Reset password request.") + @RequestBody ResetPasswordRequest resetPasswordRequest, HttpServletRequest request) throws ThingsboardException { try { - String resetToken = resetPasswordRequest.get("resetToken").asText(); - String password = resetPasswordRequest.get("password").asText(); + String resetToken = resetPasswordRequest.getResetToken(); + String password = resetPasswordRequest.getPassword(); UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); if (userCredentials != null) { systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials); @@ -273,11 +313,7 @@ public class AuthController extends BaseController { JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - ObjectMapper objectMapper = new ObjectMapper(); - ObjectNode tokenObject = objectMapper.createObjectNode(); - tokenObject.put("token", accessToken.getToken()); - tokenObject.put("refreshToken", refreshToken.getToken()); - return tokenObject; + return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); } else { throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } 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 7e8e3800f7..848ba0ed70 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -146,17 +146,15 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_PAGE_SIZE; +import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID; import static org.thingsboard.server.dao.service.Validator.validateId; @Slf4j @TbCoreComponent public abstract class BaseController { - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - protected static final String DEFAULT_DASHBOARD = "defaultDashboardId"; - protected static final String HOME_DASHBOARD = "homeDashboardId"; - - private static final int DEFAULT_PAGE_SIZE = 1000; + /*Swagger UI description*/ private static final ObjectMapper json = new ObjectMapper(); @@ -893,7 +891,7 @@ public abstract class BaseController { PageDataIterableByTenantIdEntityId relatedEdgeIdsIterator = new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE); List result = new ArrayList<>(); - for(EdgeId edgeId : relatedEdgeIdsIterator) { + for (EdgeId edgeId : relatedEdgeIdsIterator) { result.add(edgeId); } return result; diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index 81676374f8..d365d06ca5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.apache.commons.lang3.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -33,15 +35,27 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api") public class ComponentDescriptorController extends BaseController { + private static final String COMPONENT_DESCRIPTOR_DEFINITION = "Each Component Descriptor represents configuration of specific rule node (e.g. 'Save Timeseries' or 'Send Email'.). " + + "The Component Descriptors are used by the rule chain Web UI to build the configuration forms for the rule nodes. " + + "The Component Descriptors are discovered at runtime by scanning the class path and searching for @RuleNode annotation. " + + "Once discovered, the up to date list of descriptors is persisted to the database."; + + @ApiOperation(value = "Get Component Descriptor (getComponentDescriptorByClazz)", + notes = "Gets the Component Descriptor object using class name from the path parameters. " + + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/component/{componentDescriptorClazz:.+}", method = RequestMethod.GET) @ResponseBody - public ComponentDescriptor getComponentDescriptorByClazz(@PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException { + public ComponentDescriptor getComponentDescriptorByClazz( + @ApiParam(value = "Component Descriptor class name", required = true) + @PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException { checkParameter("strComponentDescriptorClazz", strComponentDescriptorClazz); try { return checkComponentDescriptorByClazz(strComponentDescriptorClazz); @@ -50,11 +64,17 @@ public class ComponentDescriptorController extends BaseController { } } + @ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByType)", + notes = "Gets the Component Descriptors using rule node type and optional rule chain type request parameters. " + + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody - public List getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType, - @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { + public List getComponentDescriptorsByType( + @ApiParam(value = "Type of the Rule Node", allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", required = true) + @PathVariable("componentType") String strComponentType, + @ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE") + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkParameter("componentType", strComponentType); try { return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType)); @@ -63,11 +83,17 @@ public class ComponentDescriptorController extends BaseController { } } + @ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByTypes)", + notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " + + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) @ResponseBody - public List getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes, - @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { + public List getComponentDescriptorsByTypes( + @ApiParam(value = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true) + @RequestParam("componentTypes") String[] strComponentTypes, + @ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE") + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException { checkArrayParameter("componentTypes", strComponentTypes); try { Set componentTypes = new HashSet<>(); diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java new file mode 100644 index 0000000000..7c881e8b67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -0,0 +1,1524 @@ +/** + * Copyright © 2016-2021 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; + +public class ControllerConstants { + + protected static final String NEW_LINE = "\n\n"; + protected static final int DEFAULT_PAGE_SIZE = 1000; + protected static final String ENTITY_TYPE = "entityType"; + 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 RPC_ID = "rpcId"; + protected static final String ENTITY_ID = "entityId"; + protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + + "See the 'Model' tab of the Response Class for more details. "; + protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the device 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 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'"; + protected static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String CUSTOMER_ID_PARAM_DESCRIPTION = "A string value representing the customer id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'"; + protected static final String RULE_CHAIN_ID_PARAM_DESCRIPTION = "A string value representing the rule chain id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String WIDGET_BUNDLE_ID_PARAM_DESCRIPTION = "A string value representing the widget bundle id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority."; + protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority."; + protected static final String TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' authority."; + protected static final String TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority."; + protected static final String CUSTOMER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'CUSTOMER_USER' authority."; + protected static final String AVAILABLE_FOR_ANY_AUTHORIZED_USER = "\n\nAvailable for any authorized user. "; + protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page"; + protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0"; + protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile"; + protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type"; + protected static final String ASSET_TYPE_DESCRIPTION = "Asset type"; + protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'"; + protected static final String RULE_CHAIN_TYPE_DESCRIPTION = "Rule chain type (CORE or EDGE)"; + protected static final String ASSET_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the asset name."; + protected static final String DASHBOARD_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the dashboard title."; + protected static final String WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the widget bundle title."; + protected static final String RPC_TEXT_SEARCH_DESCRIPTION = "Not implemented. Leave empty."; + protected static final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device name."; + protected static final String ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the entity view name."; + protected static final String USER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the user email."; + protected static final String TENANT_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant name."; + protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant profile name."; + protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the rule chain name."; + protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device profile name."; + protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the customer title."; + protected static final String EDGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the edge name."; + protected static final String EVENT_TEXT_SEARCH_DESCRIPTION = "The value is not used in searching."; + protected static final String AUDIT_LOG_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on one of the next properties: entityType, entityName, userName, actionType, actionStatus."; + protected static final String SORT_PROPERTY_DESCRIPTION = "Property of entity to sort by"; + protected static final String DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title"; + protected static final String CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, city"; + protected static final String RPC_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, expirationTime, request, response"; + protected static final String DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deviceProfileName, label, customerTitle"; + protected static final String ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type"; + protected static final String ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, customerTitle"; + protected static final String USER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, firstName, lastName, email"; + protected static final String TENANT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, state, city, address, address2, zip, phone, email"; + protected static final String TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault"; + protected static final String TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name"; + protected static final String TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, tenantProfileName, title, email, country, state, city, address, address2, zip, phone, email"; + protected static final String DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, transportType, description, isDefault"; + protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; + protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status"; + protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, id"; + 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 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"; + protected static final String RPC_STATUS_ALLOWABLE_VALUES = "QUEUED, SENT, DELIVERED, SUCCESSFUL, TIMEOUT, EXPIRED, FAILED"; + protected static final String RULE_CHAIN_TYPES_ALLOWABLE_VALUES = "CORE, EDGE"; + protected static final String TRANSPORT_TYPE_ALLOWABLE_VALUES = "DEFAULT, MQTT, COAP, LWM2M, SNMP"; + protected static final String DEVICE_INFO_DESCRIPTION = "Device Info is an extension of the default Device object that contains information about the assigned customer name and device profile name. "; + protected static final String ASSET_INFO_DESCRIPTION = "Asset Info is an extension of the default Asset object that contains information about the assigned customer name. "; + protected static final String ALARM_INFO_DESCRIPTION = "Alarm Info is an extension of the default Alarm object that also contains name of the alarm originator."; + protected static final String RELATION_INFO_DESCRIPTION = "Relation Info is an extension of the default Relation object that contains information about the 'from' and 'to' entity names. "; + protected static final String EDGE_INFO_DESCRIPTION = "Edge Info is an extension of the default Edge object that contains information about the assigned customer name. "; + protected static final String DEVICE_PROFILE_INFO_DESCRIPTION = "Device Profile Info is a lightweight object that includes main information about Device Profile excluding the heavyweight configuration object. "; + protected static final String QUEUE_SERVICE_TYPE_DESCRIPTION = "Service type (implemented only for the TB-RULE-ENGINE)"; + protected static final String QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES = "TB-RULE-ENGINE, TB-CORE, TB-TRANSPORT, JS-EXECUTOR"; + protected static final String OTA_PACKAGE_INFO_DESCRIPTION = "OTA Package Info is a lightweight object that includes main information about the OTA Package excluding the heavyweight data. "; + protected static final String OTA_PACKAGE_DESCRIPTION = "OTA Package is a heavyweight object that includes main information about the OTA Package and also data. "; + protected static final String OTA_PACKAGE_CHECKSUM_ALGORITHM_ALLOWABLE_VALUES = "MD5, SHA256, SHA384, SHA512, CRC32, MURMUR3_32, MURMUR3_128"; + protected static final String OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the ota package title."; + protected static final String OTA_PACKAGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, type, title, version, tag, url, fileName, dataSize, checksum"; + protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. "; + protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. "; + + protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the resource title."; + protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId"; + protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. "; + protected static final String LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name"; + + protected static final String DEVICE_NAME_DESCRIPTION = "A string value representing the Device name."; + protected static final String ASSET_NAME_DESCRIPTION = "A string value representing the Asset name."; + + protected static final String EVENT_START_TIME_DESCRIPTION = "Timestamp. Events with creation time before it won't be queried."; + protected static final String EVENT_END_TIME_DESCRIPTION = "Timestamp. Events with creation time after it won't be queried."; + + protected static final String EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Unassignment works in async way - first, 'unassign' notification event pushed to edge queue on platform. "; + protected static final String EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform)"; + protected static final String EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION = "Assignment works in async way - first, notification event pushed to edge service queue on platform. "; + protected static final String EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION = "(Edge will receive this instantly, if it's currently connected, or once it's going to be connected to platform)"; + + protected static final String MARKDOWN_CODE_BLOCK_START = "```json\n"; + protected static final String MARKDOWN_CODE_BLOCK_END = "\n```"; + protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"eventType\":\"ERROR\",\n" + + " \"server\":\"ip-172-31-24-152\",\n" + + " \"method\":\"onClusterEventMsg\",\n" + + " \"errorStr\":\"Error Message\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + protected static final String EVENT_LC_EVENT_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"eventType\":\"LC_EVENT\",\n" + + " \"server\":\"ip-172-31-24-152\",\n" + + " \"event\":\"STARTED\",\n" + + " \"status\":\"Success\",\n" + + " \"errorStr\":\"Error Message\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + protected static final String EVENT_STATS_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"eventType\":\"STATS\",\n" + + " \"server\":\"ip-172-31-24-152\",\n" + + " \"messagesProcessed\":10,\n" + + " \"errorsOccurred\":5\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + protected static final String DEBUG_FILTER_OBJ = + " \"msgDirectionType\":\"IN\",\n" + + " \"server\":\"ip-172-31-24-152\",\n" + + " \"dataSearch\":\"humidity\",\n" + + " \"metadataSearch\":\"deviceName\",\n" + + " \"entityName\":\"DEVICE\",\n" + + " \"relationType\":\"Success\",\n" + + " \"entityId\":\"de9d54a0-2b7a-11ec-a3cc-23386423d98f\",\n" + + " \"msgType\":\"POST_TELEMETRY_REQUEST\",\n" + + " \"isError\":\"false\",\n" + + " \"errorStr\":\"Error Message\"\n" + + "}"; + protected static final String EVENT_DEBUG_RULE_NODE_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" + + " \"eventType\":\"DEBUG_RULE_NODE\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END; + protected static final String EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" + + " \"eventType\":\"DEBUG_RULE_CHAIN\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END; + + protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. "; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION = + "{\n" + + " \"device\": {\n" + + " \"name\": \"LwRpk00000000\",\n" + + " \"type\": \"lwm2mProfileRpk\"\n" + + " },\n" + + " \"credentials\": {\n" + + " \"id\": \"null\",\n" + + " \"createdTime\": 0,\n" + + " \"deviceId\": \"null\",\n" + + " \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" + + " \"credentialsId\": \"LwRpk00000000\",\n" + + " \"credentialsValue\": {\n" + + " \"client\": {\n" + + " \"endpoint\": \"LwRpk00000000\",\n" + + " \"securityConfigClientMode\": \"RPK\",\n" + + " \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" + + " },\n" + + " \"bootstrap\": {\n" + + " \"bootstrapServer\": {\n" + + " \"securityMode\": \"RPK\",\n" + + " \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" + + " \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" + + " },\n" + + " \"lwm2mServer\": {\n" + + " \"securityMode\": \"RPK\",\n" + + " \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" + + " \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN = + MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END; + + + protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE + + "Provides a hint about the data type of the entity field that is defined in the filter key. " + + "The value type impacts the list of possible operations that you may use in the corresponding predicate. For example, you may use 'STARTS_WITH' or 'END_WITH', but you can't use 'GREATER_OR_EQUAL' for string values." + + "The following filter value types and corresponding predicate operations are supported: " + NEW_LINE + + " * 'STRING' - used to filter any 'String' or 'JSON' values. Operations: EQUAL, NOT_EQUAL, STARTS_WITH, ENDS_WITH, CONTAINS, NOT_CONTAINS; \n" + + " * 'NUMERIC' - used for 'Long' and 'Double' values. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n" + + " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" + + " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n"; + + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"schedule\":{\n" + + " \"type\":\"SPECIFIC_TIME\",\n" + + " \"endsOn\":64800000,\n" + + " \"startsOn\":43200000,\n" + + " \"timezone\":\"Europe/Kiev\",\n" + + " \"daysOfWeek\":[\n" + + " 1,\n" + + " 3,\n" + + " 5\n" + + " ]\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"schedule\":{\n" + + " \"type\":\"CUSTOM\",\n" + + " \"items\":[\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":1\n" + + " },\n" + + " {\n" + + " \"endsOn\":64800000,\n" + + " \"enabled\":true,\n" + + " \"startsOn\":43200000,\n" + + " \"dayOfWeek\":2\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":3\n" + + " },\n" + + " {\n" + + " \"endsOn\":57600000,\n" + + " \"enabled\":true,\n" + + " \"startsOn\":36000000,\n" + + " \"dayOfWeek\":4\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":5\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":6\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":7\n" + + " }\n" + + " ],\n" + + " \"timezone\":\"Europe/Kiev\"\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"spec\":{\n" + + " \"type\":\"REPEATING\",\n" + + " \"predicate\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":5,\n" + + " \"dynamicValue\":{\n" + + " \"inherit\":true,\n" + + " \"sourceType\":\"CURRENT_DEVICE\",\n" + + " \"sourceAttribute\":\"tempAttr\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"spec\":{\n" + + " \"type\":\"DURATION\",\n" + + " \"unit\":\"MINUTES\",\n" + + " \"predicate\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":30,\n" + + " \"dynamicValue\":null\n" + + " }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + + protected static final String RELATION_TYPE_PARAM_DESCRIPTION = "A string value representing relation type between entities. For example, 'Contains', 'Manages'. It can be any string value."; + protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'"; + + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + protected static final String DEFAULT_DASHBOARD = "defaultDashboardId"; + protected static final String HOME_DASHBOARD = "homeDashboardId"; + + protected static final String SINGLE_ENTITY = "\n\n## Single Entity\n\n" + + "Allows to filter only one entity based on the id. For example, this entity filter selects certain device:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"singleEntity\",\n" + + " \"singleEntity\": {\n" + + " \"id\": \"d521edb0-2a7a-11ec-94eb-213c95f54092\",\n" + + " \"entityType\": \"DEVICE\"\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String ENTITY_LIST = "\n\n## Entity List Filter\n\n" + + "Allows to filter entities of the same type using their ids. For example, this entity filter selects two devices:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"entityList\",\n" + + " \"entityType\": \"DEVICE\",\n" + + " \"entityList\": [\n" + + " \"e6501f30-2a7a-11ec-94eb-213c95f54092\",\n" + + " \"e6657bf0-2a7a-11ec-94eb-213c95f54092\"\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String ENTITY_NAME = "\n\n## Entity Name Filter\n\n" + + "Allows to filter entities of the same type using the **'starts with'** expression over entity name. " + + "For example, this entity filter selects all devices which name starts with 'Air Quality':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"entityName\",\n" + + " \"entityType\": \"DEVICE\",\n" + + " \"entityNameFilter\": \"Air Quality\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String ENTITY_TYPE_FILTER = "\n\n## Entity Type Filter\n\n" + + "Allows to filter entities based on their type (CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, etc)" + + "For example, this entity filter selects all tenant customers:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"entityType\",\n" + + " \"entityType\": \"CUSTOMER\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String ASSET_TYPE = "\n\n## Asset Type Filter\n\n" + + "Allows to filter assets based on their type and the **'starts with'** expression over their name. " + + "For example, this entity filter selects all 'charging station' assets which name starts with 'Tesla':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"assetType\",\n" + + " \"assetType\": \"charging station\",\n" + + " \"assetNameFilter\": \"Tesla\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String DEVICE_TYPE = "\n\n## Device Type Filter\n\n" + + "Allows to filter devices based on their type and the **'starts with'** expression over their name. " + + "For example, this entity filter selects all 'Temperature Sensor' devices which name starts with 'ABC':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"deviceType\",\n" + + " \"deviceType\": \"Temperature Sensor\",\n" + + " \"deviceNameFilter\": \"ABC\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String EDGE_TYPE = "\n\n## Edge Type Filter\n\n" + + "Allows to filter edge instances based on their type and the **'starts with'** expression over their name. " + + "For example, this entity filter selects all 'Factory' edge instances which name starts with 'Nevada':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"edgeType\",\n" + + " \"edgeType\": \"Factory\",\n" + + " \"edgeNameFilter\": \"Nevada\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String ENTITY_VIEW_TYPE = "\n\n## Entity View Filter\n\n" + + "Allows to filter entity views based on their type and the **'starts with'** expression over their name. " + + "For example, this entity filter selects all 'Concrete Mixer' entity views which name starts with 'CAT':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"entityViewType\",\n" + + " \"entityViewType\": \"Concrete Mixer\",\n" + + " \"entityViewNameFilter\": \"CAT\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String API_USAGE = "\n\n## Api Usage Filter\n\n" + + "Allows to query for Api Usage based on optional customer id. If the customer id is not set, returns current tenant API usage." + + "For example, this entity filter selects the 'Api Usage' entity for customer with id 'e6501f30-2a7a-11ec-94eb-213c95f54092':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"apiUsageState\",\n" + + " \"customerId\": {\n" + + " \"id\": \"d521edb0-2a7a-11ec-94eb-213c95f54092\",\n" + + " \"entityType\": \"CUSTOMER\"\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String MAX_LEVEL_DESCRIPTION = "Possible direction values are 'TO' and 'FROM'. The 'maxLevel' defines how many relation levels should the query search 'recursively'. "; + protected static final String FETCH_LAST_LEVEL_ONLY_DESCRIPTION = "Assuming the 'maxLevel' is > 1, the 'fetchLastLevelOnly' defines either to return all related entities or only entities that are on the last level of relations. "; + + protected static final String RELATIONS_QUERY_FILTER = "\n\n## Relations Query Filter\n\n" + + "Allows to filter entities that are related to the provided root entity. " + + MAX_LEVEL_DESCRIPTION + + FETCH_LAST_LEVEL_ONLY_DESCRIPTION + + "The 'filter' object allows you to define the relation type and set of acceptable entity types to search for. " + + "The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only those who match the 'filters'.\n\n" + + "For example, this entity filter selects all devices and assets which are related to the asset with id 'e51de0c0-2a7a-11ec-94eb-213c95f54092':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"relationsQuery\",\n" + + " \"rootEntity\": {\n" + + " \"entityType\": \"ASSET\",\n" + + " \"id\": \"e51de0c0-2a7a-11ec-94eb-213c95f54092\"\n" + + " },\n" + + " \"direction\": \"FROM\",\n" + + " \"maxLevel\": 1,\n" + + " \"fetchLastLevelOnly\": false,\n" + + " \"filters\": [\n" + + " {\n" + + " \"relationType\": \"Contains\",\n" + + " \"entityTypes\": [\n" + + " \"DEVICE\",\n" + + " \"ASSET\"\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + + protected static final String ASSET_QUERY_FILTER = "\n\n## Asset Search Query\n\n" + + "Allows to filter assets that are related to the provided root entity. Filters related assets based on the relation type and set of asset types. " + + MAX_LEVEL_DESCRIPTION + + FETCH_LAST_LEVEL_ONLY_DESCRIPTION + + "The 'relationType' defines the type of the relation to search for. " + + "The 'assetTypes' defines the type of the asset to search for. " + + "The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only assets that match 'relationType' and 'assetTypes' conditions.\n\n" + + "For example, this entity filter selects 'charging station' assets which are related to the asset with id 'e51de0c0-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"assetSearchQuery\",\n" + + " \"rootEntity\": {\n" + + " \"entityType\": \"ASSET\",\n" + + " \"id\": \"e51de0c0-2a7a-11ec-94eb-213c95f54092\"\n" + + " },\n" + + " \"direction\": \"FROM\",\n" + + " \"maxLevel\": 1,\n" + + " \"fetchLastLevelOnly\": false,\n" + + " \"relationType\": \"Contains\",\n" + + " \"assetTypes\": [\n" + + " \"charging station\"\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String DEVICE_QUERY_FILTER = "\n\n## Device Search Query\n\n" + + "Allows to filter devices that are related to the provided root entity. Filters related devices based on the relation type and set of device types. " + + MAX_LEVEL_DESCRIPTION + + FETCH_LAST_LEVEL_ONLY_DESCRIPTION + + "The 'relationType' defines the type of the relation to search for. " + + "The 'deviceTypes' defines the type of the device to search for. " + + "The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only devices that match 'relationType' and 'deviceTypes' conditions.\n\n" + + "For example, this entity filter selects 'Charging port' and 'Air Quality Sensor' devices which are related to the asset with id 'e52b0020-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"deviceSearchQuery\",\n" + + " \"rootEntity\": {\n" + + " \"entityType\": \"ASSET\",\n" + + " \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" + + " },\n" + + " \"direction\": \"FROM\",\n" + + " \"maxLevel\": 2,\n" + + " \"fetchLastLevelOnly\": true,\n" + + " \"relationType\": \"Contains\",\n" + + " \"deviceTypes\": [\n" + + " \"Air Quality Sensor\",\n" + + " \"Charging port\"\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String EV_QUERY_FILTER = "\n\n## Entity View Query\n\n" + + "Allows to filter entity views that are related to the provided root entity. Filters related entity views based on the relation type and set of entity view types. " + + MAX_LEVEL_DESCRIPTION + + FETCH_LAST_LEVEL_ONLY_DESCRIPTION + + "The 'relationType' defines the type of the relation to search for. " + + "The 'entityViewTypes' defines the type of the entity view to search for. " + + "The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only devices that match 'relationType' and 'deviceTypes' conditions.\n\n" + + "For example, this entity filter selects 'Concrete mixer' entity views which are related to the asset with id 'e52b0020-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"entityViewSearchQuery\",\n" + + " \"rootEntity\": {\n" + + " \"entityType\": \"ASSET\",\n" + + " \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" + + " },\n" + + " \"direction\": \"FROM\",\n" + + " \"maxLevel\": 1,\n" + + " \"fetchLastLevelOnly\": false,\n" + + " \"relationType\": \"Contains\",\n" + + " \"entityViewTypes\": [\n" + + " \"Concrete mixer\"\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String EDGE_QUERY_FILTER = "\n\n## Edge Search Query\n\n" + + "Allows to filter edge instances that are related to the provided root entity. Filters related edge instances based on the relation type and set of edge types. " + + MAX_LEVEL_DESCRIPTION + + FETCH_LAST_LEVEL_ONLY_DESCRIPTION + + "The 'relationType' defines the type of the relation to search for. " + + "The 'deviceTypes' defines the type of the device to search for. " + + "The relation query calculates all related entities, even if they are filtered using different relation types, and then extracts only devices that match 'relationType' and 'deviceTypes' conditions.\n\n" + + "For example, this entity filter selects 'Factory' edge instances which are related to the asset with id 'e52b0020-2a7a-11ec-94eb-213c95f54092' using 'Contains' relation:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"deviceSearchQuery\",\n" + + " \"rootEntity\": {\n" + + " \"entityType\": \"ASSET\",\n" + + " \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" + + " },\n" + + " \"direction\": \"FROM\",\n" + + " \"maxLevel\": 2,\n" + + " \"fetchLastLevelOnly\": true,\n" + + " \"relationType\": \"Contains\",\n" + + " \"edgeTypes\": [\n" + + " \"Factory\"\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String EMPTY = "\n\n## Entity Type Filter\n\n" + + "Allows to filter multiple entities of the same type using the **'starts with'** expression over entity name. " + + "For example, this entity filter selects all devices which name starts with 'Air Quality':\n\n" + + MARKDOWN_CODE_BLOCK_START + + "" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String ENTITY_FILTERS = + "\n\n # Entity Filters" + + "\nEntity Filter body depends on the 'type' parameter. Let's review available entity filter types. In fact, they do correspond to available dashboard aliases." + + SINGLE_ENTITY + ENTITY_LIST + ENTITY_NAME + ENTITY_TYPE_FILTER + ASSET_TYPE + DEVICE_TYPE + EDGE_TYPE + ENTITY_VIEW_TYPE + API_USAGE + RELATIONS_QUERY_FILTER + + ASSET_QUERY_FILTER + DEVICE_QUERY_FILTER + EV_QUERY_FILTER + EDGE_QUERY_FILTER; + + protected static final String FILTER_KEY = "\n\n## Filter Key\n\n" + + "Filter Key defines either entity field, attribute or telemetry. It is a JSON object that consists the key name and type. " + + "The following filter key types are supported: \n\n" + + " * 'CLIENT_ATTRIBUTE' - used for client attributes; \n" + + " * 'SHARED_ATTRIBUTE' - used for shared attributes; \n" + + " * 'SERVER_ATTRIBUTE' - used for server attributes; \n" + + " * 'ATTRIBUTE' - used for any of the above; \n" + + " * 'TIME_SERIES' - used for time-series values; \n" + + " * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type; \n" + + " * 'ALARM_FIELD' - similar to entity field, but is used in alarm queries only; \n" + + "\n\n Let's review the example:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String FILTER_PREDICATE = "\n\n## Filter Predicate\n\n" + + "Filter Predicate defines the logical expression to evaluate. The list of available operations depends on the filter value type, see above. " + + "Platform supports 4 predicate types: 'STRING', 'NUMERIC', 'BOOLEAN' and 'COMPLEX'. The last one allows to combine multiple operations over one filter key." + + "\n\nSimple predicate example to check 'value < 100': \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 100,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\nComplex predicate example, to check 'value < 10 or value > 20': \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"COMPLEX\",\n" + + " \"operation\": \"OR\",\n" + + " \"predicates\": [\n" + + " {\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 10,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " },\n" + + " {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 20,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\nMore complex predicate example, to check 'value < 10 or (value > 50 && value < 60)': \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"COMPLEX\",\n" + + " \"operation\": \"OR\",\n" + + " \"predicates\": [\n" + + " {\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 10,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " },\n" + + " {\n" + + " \"type\": \"COMPLEX\",\n" + + " \"operation\": \"AND\",\n" + + " \"predicates\": [\n" + + " {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 50,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " },\n" + + " {\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 60,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " + + "expression (for example, temperature > 'value of the tenant attribute with key 'temperatureThreshold'). " + + "It is possible to use 'dynamicValue' to define attribute of the tenant, customer or user that is performing the API call. " + + "See example below: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 0,\n" + + " \"dynamicValue\": {\n" + + " \"sourceType\": \"CURRENT_USER\",\n" + + " \"sourceAttribute\": \"temperatureThreshold\"\n" + + " }\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Note that you may use 'CURRENT_USER', 'CURRENT_CUSTOMER' and 'CURRENT_TENANT' as a 'sourceType'. The 'defaultValue' is used when the attribute with such a name is not defined for the chosen source."; + + protected static final String KEY_FILTERS = + "\n\n # Key Filters" + + "\nKey Filter allows you to define complex logical expressions over entity field, attribute or latest time-series value. The filter is defined using 'key', 'valueType' and 'predicate' objects. " + + "Single Entity Query may have zero, one or multiple predicates. If multiple filters are defined, they are evaluated using logical 'AND'. " + + "The example below checks that temperature of the entity is above 20 degrees:" + + "\n\n" + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"key\": {\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + " },\n" + + " \"valueType\": \"NUMERIC\",\n" + + " \"predicate\": {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 20,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Now let's review 'key', 'valueType' and 'predicate' objects in detail." + + FILTER_KEY + FILTER_VALUE_TYPE + FILTER_PREDICATE; + + protected static final String ENTITY_COUNT_QUERY_DESCRIPTION = + "Allows to run complex queries to search the count of platform entities (devices, assets, customers, etc) " + + "based on the combination of main entity filter and multiple key filters. Returns the number of entities that match the query definition.\n\n" + + "# Query Definition\n\n" + + "\n\nMain **entity filter** is mandatory and defines generic search criteria. " + + "For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" + + "\n\nOptional **key filters** allow to filter results of the entity filter by complex criteria against " + + "main entity fields (name, label, type, etc), attributes and telemetry. " + + "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." + + "\n\nLet's review the example:" + + "\n\n" + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"entityFilter\": {\n" + + " \"type\": \"entityType\",\n" + + " \"entityType\": \"DEVICE\"\n" + + " },\n" + + " \"keyFilters\": [\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"ATTRIBUTE\",\n" + + " \"key\": \"active\"\n" + + " },\n" + + " \"valueType\": \"BOOLEAN\",\n" + + " \"predicate\": {\n" + + " \"operation\": \"EQUAL\",\n" + + " \"value\": {\n" + + " \"defaultValue\": true,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"BOOLEAN\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Example mentioned above search all devices which have attribute 'active' set to 'true'. Now let's review available entity filters and key filters syntax:" + + ENTITY_FILTERS + + KEY_FILTERS + + ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + + protected static final String ENTITY_DATA_QUERY_DESCRIPTION = + "Allows to run complex queries over platform entities (devices, assets, customers, etc) " + + "based on the combination of main entity filter and multiple key filters. " + + "Returns the paginated result of the query that contains requested entity fields and latest values of requested attributes and time-series data.\n\n" + + "# Query Definition\n\n" + + "\n\nMain **entity filter** is mandatory and defines generic search criteria. " + + "For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" + + "\n\nOptional **key filters** allow to filter results of the **entity filter** by complex criteria against " + + "main entity fields (name, label, type, etc), attributes and telemetry. " + + "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." + + "\n\nThe **entity fields** and **latest values** contains list of entity fields and latest attribute/telemetry fields to fetch for each entity." + + "\n\nThe **page link** contains information about the page to fetch and the sort ordering." + + "\n\nLet's review the example:" + + "\n\n" + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"entityFilter\": {\n" + + " \"type\": \"entityType\",\n" + + " \"resolveMultiple\": true,\n" + + " \"entityType\": \"DEVICE\"\n" + + " },\n" + + " \"keyFilters\": [\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + " },\n" + + " \"valueType\": \"NUMERIC\",\n" + + " \"predicate\": {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 0,\n" + + " \"dynamicValue\": {\n" + + " \"sourceType\": \"CURRENT_USER\",\n" + + " \"sourceAttribute\": \"temperatureThreshold\",\n" + + " \"inherit\": false\n" + + " }\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"entityFields\": [\n" + + " {\n" + + " \"type\": \"ENTITY_FIELD\",\n" + + " \"key\": \"name\"\n" + + " },\n" + + " {\n" + + " \"type\": \"ENTITY_FIELD\",\n" + + " \"key\": \"label\"\n" + + " },\n" + + " {\n" + + " \"type\": \"ENTITY_FIELD\",\n" + + " \"key\": \"additionalInfo\"\n" + + " }\n" + + " ],\n" + + " \"latestValues\": [\n" + + " {\n" + + " \"type\": \"ATTRIBUTE\",\n" + + " \"key\": \"model\"\n" + + " },\n" + + " {\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + " }\n" + + " ],\n" + + " \"pageLink\": {\n" + + " \"page\": 0,\n" + + " \"pageSize\": 10,\n" + + " \"sortOrder\": {\n" + + " \"key\": {\n" + + " \"key\": \"name\",\n" + + " \"type\": \"ENTITY_FIELD\"\n" + + " },\n" + + " \"direction\": \"ASC\"\n" + + " }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Example mentioned above search all devices which have attribute 'active' set to 'true'. Now let's review available entity filters and key filters syntax:" + + ENTITY_FILTERS + + KEY_FILTERS + + ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + + + protected static final String ALARM_DATA_QUERY_DESCRIPTION = "This method description defines how Alarm Data Query extends the Entity Data Query. " + + "See method 'Find Entity Data by Query' first to get the info about 'Entity Data Query'." + + "\n\n The platform will first search the entities that match the entity and key filters. Then, the platform will use 'Alarm Page Link' to filter the alarms related to those entities. " + + "Finally, platform fetch the properties of alarm that are defined in the **'alarmFields'** and combine them with the other entity, attribute and latest time-series fields to return the result. " + + "\n\n See example of the alarm query below. The query will search first 100 active alarms with type 'Temperature Alarm' or 'Fire Alarm' for any device with current temperature > 0. " + + "The query will return combination of the entity fields: name of the device, device model and latest temperature reading and alarms fields: createdTime, type, severity and status: " + + "\n\n" + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"entityFilter\": {\n" + + " \"type\": \"entityType\",\n" + + " \"resolveMultiple\": true,\n" + + " \"entityType\": \"DEVICE\"\n" + + " },\n" + + " \"pageLink\": {\n" + + " \"page\": 0,\n" + + " \"pageSize\": 100,\n" + + " \"textSearch\": null,\n" + + " \"searchPropagatedAlarms\": false,\n" + + " \"statusList\": [\n" + + " \"ACTIVE\"\n" + + " ],\n" + + " \"severityList\": [\n" + + " \"CRITICAL\",\n" + + " \"MAJOR\"\n" + + " ],\n" + + " \"typeList\": [\n" + + " \"Temperature Alarm\",\n" + + " \"Fire Alarm\"\n" + + " ],\n" + + " \"sortOrder\": {\n" + + " \"key\": {\n" + + " \"key\": \"createdTime\",\n" + + " \"type\": \"ALARM_FIELD\"\n" + + " },\n" + + " \"direction\": \"DESC\"\n" + + " },\n" + + " \"timeWindow\": 86400000\n" + + " },\n" + + " \"keyFilters\": [\n" + + " {\n" + + " \"key\": {\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + " },\n" + + " \"valueType\": \"NUMERIC\",\n" + + " \"predicate\": {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"defaultValue\": 0,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"alarmFields\": [\n" + + " {\n" + + " \"type\": \"ALARM_FIELD\",\n" + + " \"key\": \"createdTime\"\n" + + " },\n" + + " {\n" + + " \"type\": \"ALARM_FIELD\",\n" + + " \"key\": \"type\"\n" + + " },\n" + + " {\n" + + " \"type\": \"ALARM_FIELD\",\n" + + " \"key\": \"severity\"\n" + + " },\n" + + " {\n" + + " \"type\": \"ALARM_FIELD\",\n" + + " \"key\": \"status\"\n" + + " }\n" + + " ],\n" + + " \"entityFields\": [\n" + + " {\n" + + " \"type\": \"ENTITY_FIELD\",\n" + + " \"key\": \"name\"\n" + + " }\n" + + " ],\n" + + " \"latestValues\": [\n" + + " {\n" + + " \"type\": \"ATTRIBUTE\",\n" + + " \"key\": \"model\"\n" + + " },\n" + + " {\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + ""; + + protected static final String COAP_TRANSPORT_CONFIGURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\":\"COAP\",\n" + + " \"clientSettings\":{\n" + + " \"edrxCycle\":null,\n" + + " \"powerMode\":\"DRX\",\n" + + " \"psmActivityTimer\":null,\n" + + " \"pagingTransmissionWindow\":null\n" + + " },\n" + + " \"coapDeviceTypeConfiguration\":{\n" + + " \"coapDeviceType\":\"DEFAULT\",\n" + + " \"transportPayloadTypeConfiguration\":{\n" + + " \"transportPayloadType\":\"JSON\"\n" + + " }\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + + protected static final String TRANSPORT_CONFIGURATION = "# Transport Configuration" + NEW_LINE + + "5 transport configuration types are available:\n" + + " * 'DEFAULT';\n" + + " * 'MQTT';\n" + + " * 'LWM2M';\n" + + " * 'COAP';\n" + + " * 'SNMP'." + NEW_LINE + "Default type supports basic MQTT, HTTP, CoAP and LwM2M transports. " + + "Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-profiles/#transport-configuration) for more details about other types.\n" + + "\nSee another example of COAP transport configuration below:" + NEW_LINE + COAP_TRANSPORT_CONFIGURATION_EXAMPLE; + + protected static final String ALARM_FILTER_KEY = "## Alarm Filter Key" + NEW_LINE + + "Filter Key defines either entity field, attribute, telemetry or constant. It is a JSON object that consists the key name and type. The following filter key types are supported:\n" + + " * 'ATTRIBUTE' - used for attributes values;\n" + + " * 'TIME_SERIES' - used for time-series values;\n" + + " * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type;\n" + + " * 'CONSTANT' - constant value specified." + NEW_LINE + "Let's review the example:" + NEW_LINE + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"TIME_SERIES\",\n" + + " \"key\": \"temperature\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END; + + protected static final String DEVICE_PROFILE_FILTER_PREDICATE = NEW_LINE + "## Filter Predicate" + NEW_LINE + + "Filter Predicate defines the logical expression to evaluate. The list of available operations depends on the filter value type, see above. " + + "Platform supports 4 predicate types: 'STRING', 'NUMERIC', 'BOOLEAN' and 'COMPLEX'. The last one allows to combine multiple operations over one filter key." + NEW_LINE + + "Simple predicate example to check 'value < 100': " + NEW_LINE + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 100,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + NEW_LINE + + "Complex predicate example, to check 'value < 10 or value > 20': " + NEW_LINE + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"COMPLEX\",\n" + + " \"operation\": \"OR\",\n" + + " \"predicates\": [\n" + + " {\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 10,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " },\n" + + " {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 20,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + NEW_LINE + + "More complex predicate example, to check 'value < 10 or (value > 50 && value < 60)': " + NEW_LINE + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"type\": \"COMPLEX\",\n" + + " \"operation\": \"OR\",\n" + + " \"predicates\": [\n" + + " {\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 10,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " },\n" + + " {\n" + + " \"type\": \"COMPLEX\",\n" + + " \"operation\": \"AND\",\n" + + " \"predicates\": [\n" + + " {\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 50,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " },\n" + + " {\n" + + " \"operation\": \"LESS\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 60,\n" + + " \"dynamicValue\": null\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + NEW_LINE + + "You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " + + "expression (for example, temperature > value of the tenant attribute with key 'temperatureThreshold'). " + + "It is possible to use 'dynamicValue' to define attribute of the tenant, customer or device. " + + "See example below:" + NEW_LINE + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"operation\": \"GREATER\",\n" + + " \"value\": {\n" + + " \"userValue\": null,\n" + + " \"defaultValue\": 0,\n" + + " \"dynamicValue\": {\n" + + " \"inherit\": false,\n" + + " \"sourceType\": \"CURRENT_TENANT\",\n" + + " \"sourceAttribute\": \"temperatureThreshold\"\n" + + " }\n" + + " },\n" + + " \"type\": \"NUMERIC\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + NEW_LINE + + "Note that you may use 'CURRENT_DEVICE', 'CURRENT_CUSTOMER' and 'CURRENT_TENANT' as a 'sourceType'. The 'defaultValue' is used when the attribute with such a name is not defined for the chosen source. " + + "The 'sourceAttribute' can be inherited from the owner of the specified 'sourceType' if 'inherit' is set to true."; + + protected static final String KEY_FILTERS_DESCRIPTION = "# Key Filters" + NEW_LINE + + "Key filter objects are created under the **'condition'** array. They allow you to define complex logical expressions over entity field, " + + "attribute, latest time-series value or constant. The filter is defined using 'key', 'valueType', " + + "'value' (refers to the value of the 'CONSTANT' alarm filter key type) and 'predicate' objects. Let's review each object:" + NEW_LINE + + ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE; + + protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + + " \"alarms\":[\n" + + " ],\n" + + " \"configuration\":{\n" + + " \"type\":\"DEFAULT\"\n" + + " },\n" + + " \"provisionConfiguration\":{\n" + + " \"type\":\"DISABLED\",\n" + + " \"provisionDeviceSecret\":null\n" + + " },\n" + + " \"transportConfiguration\":{\n" + + " \"type\":\"DEFAULT\"\n" + + " }\n" + + "}" + MARKDOWN_CODE_BLOCK_END; + + protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" + + " \"alarms\":[\n" + + " {\n" + + " \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" + + " \"alarmType\":\"Temperature Alarm\",\n" + + " \"clearRule\":{\n" + + " \"schedule\":null,\n" + + " \"condition\":{\n" + + " \"spec\":{\n" + + " \"type\":\"SIMPLE\"\n" + + " },\n" + + " \"condition\":[\n" + + " {\n" + + " \"key\":{\n" + + " \"key\":\"temperature\",\n" + + " \"type\":\"TIME_SERIES\"\n" + + " },\n" + + " \"value\":null,\n" + + " \"predicate\":{\n" + + " \"type\":\"NUMERIC\",\n" + + " \"value\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":30.0,\n" + + " \"dynamicValue\":null\n" + + " },\n" + + " \"operation\":\"LESS\"\n" + + " },\n" + + " \"valueType\":\"NUMERIC\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"dashboardId\":null,\n" + + " \"alarmDetails\":null\n" + + " },\n" + + " \"propagate\":false,\n" + + " \"createRules\":{\n" + + " \"MAJOR\":{\n" + + " \"schedule\":{\n" + + " \"type\":\"SPECIFIC_TIME\",\n" + + " \"endsOn\":64800000,\n" + + " \"startsOn\":43200000,\n" + + " \"timezone\":\"Europe/Kiev\",\n" + + " \"daysOfWeek\":[\n" + + " 1,\n" + + " 3,\n" + + " 5\n" + + " ]\n" + + " },\n" + + " \"condition\":{\n" + + " \"spec\":{\n" + + " \"type\":\"DURATION\",\n" + + " \"unit\":\"MINUTES\",\n" + + " \"predicate\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":30,\n" + + " \"dynamicValue\":null\n" + + " }\n" + + " },\n" + + " \"condition\":[\n" + + " {\n" + + " \"key\":{\n" + + " \"key\":\"temperature\",\n" + + " \"type\":\"TIME_SERIES\"\n" + + " },\n" + + " \"value\":null,\n" + + " \"predicate\":{\n" + + " \"type\":\"COMPLEX\",\n" + + " \"operation\":\"OR\",\n" + + " \"predicates\":[\n" + + " {\n" + + " \"type\":\"NUMERIC\",\n" + + " \"value\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":50.0,\n" + + " \"dynamicValue\":null\n" + + " },\n" + + " \"operation\":\"LESS_OR_EQUAL\"\n" + + " },\n" + + " {\n" + + " \"type\":\"NUMERIC\",\n" + + " \"value\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":30.0,\n" + + " \"dynamicValue\":null\n" + + " },\n" + + " \"operation\":\"GREATER\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"valueType\":\"NUMERIC\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"dashboardId\":null,\n" + + " \"alarmDetails\":null\n" + + " },\n" + + " \"WARNING\":{\n" + + " \"schedule\":{\n" + + " \"type\":\"CUSTOM\",\n" + + " \"items\":[\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":1\n" + + " },\n" + + " {\n" + + " \"endsOn\":64800000,\n" + + " \"enabled\":true,\n" + + " \"startsOn\":43200000,\n" + + " \"dayOfWeek\":2\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":3\n" + + " },\n" + + " {\n" + + " \"endsOn\":57600000,\n" + + " \"enabled\":true,\n" + + " \"startsOn\":36000000,\n" + + " \"dayOfWeek\":4\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":5\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":6\n" + + " },\n" + + " {\n" + + " \"endsOn\":0,\n" + + " \"enabled\":false,\n" + + " \"startsOn\":0,\n" + + " \"dayOfWeek\":7\n" + + " }\n" + + " ],\n" + + " \"timezone\":\"Europe/Kiev\"\n" + + " },\n" + + " \"condition\":{\n" + + " \"spec\":{\n" + + " \"type\":\"REPEATING\",\n" + + " \"predicate\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":5,\n" + + " \"dynamicValue\":null\n" + + " }\n" + + " },\n" + + " \"condition\":[\n" + + " {\n" + + " \"key\":{\n" + + " \"key\":\"tempConstant\",\n" + + " \"type\":\"CONSTANT\"\n" + + " },\n" + + " \"value\":30,\n" + + " \"predicate\":{\n" + + " \"type\":\"NUMERIC\",\n" + + " \"value\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":0.0,\n" + + " \"dynamicValue\":{\n" + + " \"inherit\":false,\n" + + " \"sourceType\":\"CURRENT_DEVICE\",\n" + + " \"sourceAttribute\":\"tempThreshold\"\n" + + " }\n" + + " },\n" + + " \"operation\":\"EQUAL\"\n" + + " },\n" + + " \"valueType\":\"NUMERIC\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"dashboardId\":null,\n" + + " \"alarmDetails\":null\n" + + " },\n" + + " \"CRITICAL\":{\n" + + " \"schedule\":null,\n" + + " \"condition\":{\n" + + " \"spec\":{\n" + + " \"type\":\"SIMPLE\"\n" + + " },\n" + + " \"condition\":[\n" + + " {\n" + + " \"key\":{\n" + + " \"key\":\"temperature\",\n" + + " \"type\":\"TIME_SERIES\"\n" + + " },\n" + + " \"value\":null,\n" + + " \"predicate\":{\n" + + " \"type\":\"NUMERIC\",\n" + + " \"value\":{\n" + + " \"userValue\":null,\n" + + " \"defaultValue\":50.0,\n" + + " \"dynamicValue\":null\n" + + " },\n" + + " \"operation\":\"GREATER\"\n" + + " },\n" + + " \"valueType\":\"NUMERIC\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"dashboardId\":null,\n" + + " \"alarmDetails\":null\n" + + " }\n" + + " },\n" + + " \"propagateRelationTypes\":null\n" + + " }\n" + + " ],\n" + + " \"configuration\":{\n" + + " \"type\":\"DEFAULT\"\n" + + " },\n" + + " \"provisionConfiguration\":{\n" + + " \"type\":\"ALLOW_CREATE_NEW_DEVICES\",\n" + + " \"provisionDeviceSecret\":\"vaxb9hzqdbz3oqukvomg\"\n" + + " },\n" + + " \"transportConfiguration\":{\n" + + " \"type\":\"MQTT\",\n" + + " \"deviceTelemetryTopic\":\"v1/devices/me/telemetry\",\n" + + " \"deviceAttributesTopic\":\"v1/devices/me/attributes\",\n" + + " \"transportPayloadTypeConfiguration\":{\n" + + " \"transportPayloadType\":\"PROTOBUF\",\n" + + " \"deviceTelemetryProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage telemetry;\\n\\nmessage SensorDataReading {\\n\\n optional double temperature = 1;\\n optional double humidity = 2;\\n InnerObject innerObject = 3;\\n\\n message InnerObject {\\n optional string key1 = 1;\\n optional bool key2 = 2;\\n optional double key3 = 3;\\n optional int32 key4 = 4;\\n optional string key5 = 5;\\n }\\n}\",\n" + + " \"deviceAttributesProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage attributes;\\n\\nmessage SensorConfiguration {\\n optional string firmwareVersion = 1;\\n optional string serialNumber = 2;\\n}\",\n" + + " \"deviceRpcRequestProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage rpc;\\n\\nmessage RpcRequestMsg {\\n optional string method = 1;\\n optional int32 requestId = 2;\\n optional string params = 3;\\n}\",\n" + + " \"deviceRpcResponseProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage rpc;\\n\\nmessage RpcResponseMsg {\\n optional string payload = 1;\\n}\"\n" + + " }\n" + + " }\n" + + "}" + MARKDOWN_CODE_BLOCK_END; + protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE + + "Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " + + "First one is the default device profile data configuration and second one - the custom one. " + + NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE + + NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:"; + + protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE + + "Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " + + NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " + + "**'daysOfWeek'** field represents Monday as 1, Tuesday as 2 and so on. **'startsOn'** and **'endsOn'** fields represent hours in millis (e.g. 64800000 = 18:00 or 6pm). " + + "**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE + + "## Specific Time Schedule" + NEW_LINE + + DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE + + "## Custom Schedule" + + NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE; + + protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE + + "Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE + + "Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " + + "or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" + + "\n**'sourceType'** of the **'sourceAttribute'** can be: \n" + + " * 'CURRENT_DEVICE';\n" + + " * 'CURRENT_CUSTOMER';\n" + + " * 'CURRENT_TENANT'." + NEW_LINE + + "**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE + + "## Repeating alarm condition" + NEW_LINE + + DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE + + "## Duration alarm condition" + NEW_LINE + + DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE + + "**'unit'** can be: \n" + + " * 'SECONDS';\n" + + " * 'MINUTES';\n" + + " * 'HOURS';\n" + + " * 'DAYS'." + NEW_LINE; + + protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE + + "There are 3 types of device provision configuration for the device profile: \n" + + " * 'DISABLED';\n" + + " * 'ALLOW_CREATE_NEW_DEVICES';\n" + + " * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE + + "Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE; + + protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE + + KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION; + + protected static final String DEVICE_PROFILE_ID = "deviceProfileId"; + + protected static final String MODEL_DESCRIPTION = "See the 'Model' tab for more details."; + protected static final String ENTITY_VIEW_DESCRIPTION = "Entity Views limit the degree of exposure of the Device or Asset telemetry and attributes to the Customers. " + + "Every Entity View references exactly one entity (device or asset) and defines telemetry and attribute keys that will be visible to the assigned Customer. " + + "As a Tenant Administrator you are able to create multiple EVs per Device or Asset and assign them to different Customers. "; + protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION; + + protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'."; + protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'."; + protected static final String ATTRIBUTES_SCOPE_ALLOWED_VALUES = "SERVER_SCOPE, CLIENT_SCOPE, SHARED_SCOPE"; + protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details."; + + protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys."; + protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest timeseries. For example, 'temperature,humidity'."; + protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility"; + protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details."; + + + protected static final String STRICT_DATA_TYPES_DESCRIPTION = "Enables/disables conversion of telemetry values to strings. Conversion is enabled by default. Set parameter to 'true' in order to disable the conversion."; + protected static final String INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION = "Referencing a non-existing entity Id or invalid entity type will cause an error. "; + + protected static final String SAVE_ATTIRIBUTES_STATUS_OK = "Attribute from the request was created or updated. "; + protected static final String INVALID_STRUCTURE_OF_THE_REQUEST = "Invalid structure of the request"; + protected static final String SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST = INVALID_STRUCTURE_OF_THE_REQUEST + " or invalid attributes scope provided."; + protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_OK = "Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED', " + + "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."; + protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED = "User is not authorized to save entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."; + protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " + + "Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."; + protected static final String SAVE_ENTITY_TIMESERIES_STATUS_OK = "Timeseries from the request was created or updated. " + + "Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED'."; + protected static final String SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED = "User is not authorized to save entity timeseries for selected entity. Most likely, User belongs to different Customer or Tenant."; + protected static final String SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " + + "Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace."; + + protected static final String ENTITY_ATTRIBUTE_SCOPES = " List of possible attribute scopes depends on the entity type: " + + "\n\n * SERVER_SCOPE - supported for all entity types;" + + "\n * CLIENT_SCOPE - supported for devices;" + + "\n * SHARED_SCOPE - supported for devices. "+ "\n\n"; + + protected static final String ATTRIBUTE_DATA_EXAMPLE = "[\n" + + " {\"key\": \"stringAttributeKey\", \"value\": \"value\", \"lastUpdateTs\": 1609459200000},\n" + + " {\"key\": \"booleanAttributeKey\", \"value\": false, \"lastUpdateTs\": 1609459200001},\n" + + " {\"key\": \"doubleAttributeKey\", \"value\": 42.2, \"lastUpdateTs\": 1609459200002},\n" + + " {\"key\": \"longKeyExample\", \"value\": 73, \"lastUpdateTs\": 1609459200003},\n" + + " {\"key\": \"jsonKeyExample\",\n" + + " \"value\": {\n" + + " \"someNumber\": 42,\n" + + " \"someArray\": [1,2,3],\n" + + " \"someNestedObject\": {\"key\": \"value\"}\n" + + " },\n" + + " \"lastUpdateTs\": 1609459200004\n" + + " }\n" + + "]"; + + protected static final String LATEST_TS_STRICT_DATA_EXAMPLE = "{\n" + + " \"stringTsKey\": [{ \"value\": \"value\", \"ts\": 1609459200000}],\n" + + " \"booleanTsKey\": [{ \"value\": false, \"ts\": 1609459200000}],\n" + + " \"doubleTsKey\": [{ \"value\": 42.2, \"ts\": 1609459200000}],\n" + + " \"longTsKey\": [{ \"value\": 73, \"ts\": 1609459200000}],\n" + + " \"jsonTsKey\": [{ \n" + + " \"value\": {\n" + + " \"someNumber\": 42,\n" + + " \"someArray\": [1,2,3],\n" + + " \"someNestedObject\": {\"key\": \"value\"}\n" + + " }, \n" + + " \"ts\": 1609459200000}]\n" + + "}\n"; + + protected static final String LATEST_TS_NON_STRICT_DATA_EXAMPLE = "{\n" + + " \"stringTsKey\": [{ \"value\": \"value\", \"ts\": 1609459200000}],\n" + + " \"booleanTsKey\": [{ \"value\": \"false\", \"ts\": 1609459200000}],\n" + + " \"doubleTsKey\": [{ \"value\": \"42.2\", \"ts\": 1609459200000}],\n" + + " \"longTsKey\": [{ \"value\": \"73\", \"ts\": 1609459200000}],\n" + + " \"jsonTsKey\": [{ \"value\": \"{\\\"someNumber\\\": 42,\\\"someArray\\\": [1,2,3],\\\"someNestedObject\\\": {\\\"key\\\": \\\"value\\\"}}\", \"ts\": 1609459200000}]\n" + + "}\n"; + + protected static final String TS_STRICT_DATA_EXAMPLE = "{\n" + + " \"temperature\": [\n" + + " {\n" + + " \"value\": 36.7,\n" + + " \"ts\": 1609459200000\n" + + " },\n" + + " {\n" + + " \"value\": 36.6,\n" + + " \"ts\": 1609459201000\n" + + " }\n" + + " ]\n" + + "}"; + + protected static final String SAVE_ATTRIBUTES_REQUEST_PAYLOAD = "The request payload is a JSON object with key-value format of attributes to create or update. " + + "For example:\n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"stringKey\":\"value1\", \n" + + " \"booleanKey\":true, \n" + + " \"doubleKey\":42.0, \n" + + " \"longKey\":73, \n" + + " \"jsonKey\": {\n" + + " \"someNumber\": 42,\n" + + " \"someArray\": [1,2,3],\n" + + " \"someNestedObject\": {\"key\": \"value\"}\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + "\n"; + + protected static final String SAVE_TIMESERIES_REQUEST_PAYLOAD = "The request payload is a JSON document with three possible formats:\n\n" + + "Simple format without timestamp. In such a case, current server time will be used: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\"temperature\": 26}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Single JSON object with timestamp: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n JSON array with timestamps: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "[{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, {\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}]" + + MARKDOWN_CODE_BLOCK_END ; +} diff --git a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java index 3e58e0656e..12a5c74588 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -18,6 +18,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -45,23 +47,43 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api") public class CustomerController extends BaseController { - public static final String CUSTOMER_ID = "customerId"; public static final String IS_PUBLIC = "isPublic"; + public static final String CUSTOMER_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the customer is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the user belongs to the customer."; + @ApiOperation(value = "Get Customer (getCustomerById)", + notes = "Get the Customer object based on the provided Customer Id. " + + CUSTOMER_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}", method = RequestMethod.GET) @ResponseBody - public Customer getCustomerById(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { + public Customer getCustomerById( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); Customer customer = checkCustomerId(customerId, Operation.READ); - if(!customer.getAdditionalInfo().isNull()) { + if (!customer.getAdditionalInfo().isNull()) { processDashboardIdFromAdditionalInfo((ObjectNode) customer.getAdditionalInfo(), HOME_DASHBOARD); } return customer; @@ -70,10 +92,16 @@ public class CustomerController extends BaseController { } } + + @ApiOperation(value = "Get short Customer info (getShortCustomerInfoById)", + notes = "Get the short customer object that contains only the title and 'isPublic' flag. " + + CUSTOMER_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET) @ResponseBody - public JsonNode getShortCustomerInfoById(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { + public JsonNode getShortCustomerInfoById( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); @@ -88,10 +116,15 @@ public class CustomerController extends BaseController { } } + @ApiOperation(value = "Get Customer Title (getCustomerTitleById)", + notes = "Get the title of the customer. " + + CUSTOMER_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text") @ResponseBody - public String getCustomerTitleById(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { + public String getCustomerTitleById( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); @@ -102,10 +135,15 @@ public class CustomerController extends BaseController { } } + @ApiOperation(value = "Create or update Customer (saveCustomer)", + notes = "Creates or Updates the Customer. When creating customer, platform generates Customer Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address) " + + "The newly created Customer Id will be present in the response. " + + "Specify existing Customer Id to update the Customer. " + + "Referencing non-existing Customer Id will cause 'Not Found' error." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody - public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException { + public Customer saveCustomer(@ApiParam(value = "A JSON value representing the customer.") @RequestBody Customer customer) throws ThingsboardException { try { customer.setTenantId(getCurrentUser().getTenantId()); @@ -131,10 +169,15 @@ public class CustomerController extends BaseController { } } + @ApiOperation(value = "Delete Customer (deleteCustomer)", + notes = "Deletes the Customer and all customer Users. " + + "All assigned Dashboards, Assets, Devices, etc. will be unassigned but not deleted. " + + "Referencing non-existing Customer Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { + public void deleteCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); @@ -161,14 +204,23 @@ public class CustomerController extends BaseController { } } + @ApiOperation(value = "Get Tenant Customers (getCustomers)", + notes = "Returns a page of customers owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getCustomers(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getCustomers( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = CUSTOMER_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); TenantId tenantId = getCurrentUser().getTenantId(); @@ -178,10 +230,13 @@ public class CustomerController extends BaseController { } } + @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", + notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET) @ResponseBody public Customer getTenantCustomer( + @ApiParam(value = "A string value representing the Customer title.") @RequestParam String customerTitle) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index f219889403..8b3647efe2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -17,8 +17,11 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -28,6 +31,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.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; @@ -47,7 +51,6 @@ import org.thingsboard.server.common.data.id.EdgeId; 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.common.util.JacksonUtil; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.model.SecurityUser; @@ -59,6 +62,27 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +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_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -68,11 +92,16 @@ public class DashboardController extends BaseController { private static final String HOME_DASHBOARD_ID = "homeDashboardId"; private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar"; + public static final String DASHBOARD_INFO_DEFINITION = "The Dashboard Info object contains lightweight information about the dashboard (e.g. title, image, assigned customers) but does not contain the heavyweight configuration JSON."; + public static final String DASHBOARD_DEFINITION = "The Dashboard object is a heavyweight object that contains information about the dashboard (e.g. title, image, assigned customers) and also configuration JSON (e.g. layouts, widgets, entity aliases)."; + public static final String HIDDEN_FOR_MOBILE = "Exclude dashboards that are hidden for mobile"; - @Value("${dashboard.max_datapoints_limit}") + @Value("${ui.dashboard.max_datapoints_limit}") private long maxDatapointsLimit; - + @ApiOperation(value = "Get server time (getServerTime)", + notes = "Get the server time (milliseconds since January 1, 1970 UTC). " + + "Used to adjust view of the dashboards according to the difference between browser and server time.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET) @ResponseBody @@ -80,6 +109,11 @@ public class DashboardController extends BaseController { return System.currentTimeMillis(); } + @ApiOperation(value = "Get max data points limit (getMaxDatapointsLimit)", + notes = "Get the maximum number of data points that dashboard may request from the server per in a single subscription command. " + + "This value impacts the time window behavior. It impacts 'Max values' parameter in case user selects 'None' as 'Data aggregation function'. " + + "It also impacts the 'Grouping interval' in case of any other 'Data aggregation function' is selected. " + + "The actual value of the limit is configurable in the system configuration file.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/maxDatapointsLimit", method = RequestMethod.GET) @ResponseBody @@ -87,10 +121,16 @@ public class DashboardController extends BaseController { return maxDatapointsLimit; } + @ApiOperation(value = "Get Dashboard Info (getDashboardInfoById)", + notes = "Get the information about the dashboard based on 'dashboardId' parameter. " + DASHBOARD_INFO_DEFINITION, + produces = MediaType.APPLICATION_JSON_VALUE + ) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET) @ResponseBody - public DashboardInfo getDashboardInfoById(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public DashboardInfo getDashboardInfoById( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -100,10 +140,16 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Dashboard (getDashboardById)", + notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE + ) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET) @ResponseBody - public Dashboard getDashboardById(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public Dashboard getDashboardById( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -113,10 +159,20 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)", + notes = "Create or update the Dashboard. When creating dashboard, platform generates Dashboard Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address)." + + "The newly created Dashboard id will be present in the response. " + + "Specify existing Dashboard id to update the dashboard. " + + "Referencing non-existing dashboard Id will cause 'Not Found' error. " + + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/dashboard", method = RequestMethod.POST) @ResponseBody - public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException { + public Dashboard saveDashboard( + @ApiParam(value = "A JSON value representing the dashboard.") + @RequestBody Dashboard dashboard) throws ThingsboardException { try { dashboard.setTenantId(getCurrentUser().getTenantId()); @@ -141,10 +197,14 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Delete the Dashboard (deleteDashboard)", + notes = "Delete the Dashboard." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteDashboard(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public void deleteDashboard( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -170,12 +230,19 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Assign the Dashboard (assignDashboardToCustomer)", + notes = "Assign the Dashboard to specified Customer or do nothing if the Dashboard is already assigned to that Customer. " + + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.POST) @ResponseBody - public Dashboard assignDashboardToCustomer(@PathVariable("customerId") String strCustomerId, - @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { - checkParameter("customerId", strCustomerId); + public Dashboard assignDashboardToCustomer( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter(CUSTOMER_ID, strCustomerId); checkParameter(DASHBOARD_ID, strDashboardId); try { CustomerId customerId = new CustomerId(toUUID(strCustomerId)); @@ -203,11 +270,18 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Unassign the Dashboard (unassignDashboardFromCustomer)", + notes = "Unassign the Dashboard from specified Customer or do nothing if the Dashboard is already assigned to that Customer. " + + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) @ResponseBody - public Dashboard unassignDashboardFromCustomer(@PathVariable("customerId") String strCustomerId, - @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public Dashboard unassignDashboardFromCustomer( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(DASHBOARD_ID, strDashboardId); try { @@ -235,11 +309,20 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Update the Dashboard Customers (updateDashboardCustomers)", + notes = "Updates the list of Customers that this Dashboard is assigned to. Removes previous assignments to customers that are not in the provided list. " + + "Returns the Dashboard object. " + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/dashboard/{dashboardId}/customers", method = RequestMethod.POST) @ResponseBody - public Dashboard updateDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId, - @RequestBody(required = false) String[] strCustomerIds) throws ThingsboardException { + public Dashboard updateDashboardCustomers( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId, + @ApiParam(value = "JSON array with the list of customer ids, or empty to remove all customers") + @RequestBody(required = false) String[] strCustomerIds) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -301,11 +384,19 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Adds the Dashboard Customers (addDashboardCustomers)", + notes = "Adds the list of Customers to the existing list of assignments for the Dashboard. Keeps previous assignments to customers that are not in the provided list. " + + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/dashboard/{dashboardId}/customers/add", method = RequestMethod.POST) @ResponseBody - public Dashboard addDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId, - @RequestBody String[] strCustomerIds) throws ThingsboardException { + public Dashboard addDashboardCustomers( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId, + @ApiParam(value = "JSON array with the list of customer ids") + @RequestBody String[] strCustomerIds) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -345,11 +436,19 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Remove the Dashboard Customers (removeDashboardCustomers)", + notes = "Removes the list of Customers from the existing list of assignments for the Dashboard. Keeps other assignments to customers that are not in the provided list. " + + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE, + consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/dashboard/{dashboardId}/customers/remove", method = RequestMethod.POST) @ResponseBody - public Dashboard removeDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId, - @RequestBody String[] strCustomerIds) throws ThingsboardException { + public Dashboard removeDashboardCustomers( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId, + @ApiParam(value = "JSON array with the list of customer ids") + @RequestBody String[] strCustomerIds) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -389,10 +488,20 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Assign the Dashboard to Public Customer (assignDashboardToPublicCustomer)", + notes = "Assigns the dashboard to a special, auto-generated 'Public' Customer. Once assigned, unauthenticated users may browse the dashboard. " + + "This method is useful if you like to embed the dashboard on public web pages to be available for users that are not logged in. " + + "Be aware that making the dashboard public does not mean that it automatically makes all devices and assets you use in the dashboard to be public." + + "Use [assign Asset to Public Customer](#!/asset-controller/assignAssetToPublicCustomerUsingPOST) and " + + "[assign Device to Public Customer](#!/device-controller/assignDeviceToPublicCustomerUsingPOST) for this purpose. " + + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) @ResponseBody - public Dashboard assignDashboardToPublicCustomer(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public Dashboard assignDashboardToPublicCustomer( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -415,10 +524,16 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Unassign the Dashboard from Public Customer (unassignDashboardFromPublicCustomer)", + notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " + + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.DELETE) @ResponseBody - public Dashboard unassignDashboardFromPublicCustomer(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public Dashboard unassignDashboardFromPublicCustomer( + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION) + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { checkParameter(DASHBOARD_ID, strDashboardId); try { DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); @@ -442,15 +557,25 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)", + notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + + SYSTEM_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantDashboards( - @PathVariable("tenantId") String strTenantId, + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(TENANT_ID) String strTenantId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = DASHBOARD_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = new TenantId(toUUID(strTenantId)); @@ -462,20 +587,30 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Tenant Dashboards (getTenantDashboards)", + notes = "Returns a page of dashboard info objects owned by the tenant of a current user. " + + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantDashboards( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = HIDDEN_FOR_MOBILE) @RequestParam(required = false) Boolean mobile, + @ApiParam(value = DASHBOARD_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (mobile != null && mobile.booleanValue()) { + if (mobile != null && mobile) { return checkNotNull(dashboardService.findMobileDashboardsByTenantId(tenantId, pageLink)); } else { return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink)); @@ -485,24 +620,35 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Customer Dashboards (getCustomerDashboards)", + notes = "Returns a page of dashboard info objects owned by the specified customer. " + + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerDashboards( - @PathVariable("customerId") String strCustomerId, + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = HIDDEN_FOR_MOBILE) @RequestParam(required = false) Boolean mobile, + @ApiParam(value = DASHBOARD_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - checkParameter("customerId", strCustomerId); + checkParameter(CUSTOMER_ID, strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId, Operation.READ); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); - if (mobile != null && mobile.booleanValue()) { + if (mobile != null && mobile) { return checkNotNull(dashboardService.findMobileDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } else { return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); @@ -512,6 +658,12 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Home Dashboard (getHomeDashboard)", + notes = "Returns the home dashboard object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the User. " + + "If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " + + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET) @ResponseBody @@ -543,6 +695,12 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Home Dashboard Info (getHomeDashboardInfo)", + notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the User. " + + "If 'homeDashboardId' parameter is not set on the User level and the User has authority 'CUSTOMER_USER', check the same parameter for the corresponding Customer. " + + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) @ResponseBody @@ -574,6 +732,10 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Tenant Home Dashboard Info (getTenantHomeDashboardInfo)", + notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the corresponding tenant. " + + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) @ResponseBody @@ -596,10 +758,16 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Update Tenant Home Dashboard Info (getTenantHomeDashboardInfo)", + notes = "Update the home dashboard assignment for the current tenant. " + + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void setTenantHomeDashboardInfo(@RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException { + public void setTenantHomeDashboardInfo( + @ApiParam(value = "A JSON object that represents home dashboard id and other parameters", required = true) + @RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException { try { if (homeDashboardInfo.getDashboardId() != null) { checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ); @@ -635,7 +803,8 @@ public class DashboardController extends BaseController { } return new HomeDashboardInfo(dashboardId, hideDashboardToolbar); } - } catch (Exception e) {} + } catch (Exception e) { + } return null; } @@ -651,10 +820,19 @@ public class DashboardController extends BaseController { } return new HomeDashboard(dashboard, hideDashboardToolbar); } - } catch (Exception e) {} + } catch (Exception e) { + } return null; } + @ApiOperation(value = "Assign dashboard to edge (assignDashboardToEdge)", + notes = "Creates assignment of an existing dashboard to an instance of The Edge. " + + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive a copy of assignment dashboard " + + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once dashboard will be delivered to edge service, it's going to be available for usage on remote edge instance." + + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST) @ResponseBody @@ -688,6 +866,14 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)", + notes = "Clears assignment of the dashboard to the edge. " + + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive an 'unassign' command to remove dashboard " + + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once 'unassign' command will be delivered to edge service, it's going to remove dashboard locally." + + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 8cfa5ee2cf..07d6bf06c1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -19,9 +19,12 @@ 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.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -43,6 +46,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.device.DeviceSearchQuery; @@ -83,6 +87,31 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_NAME_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @RestController @@ -91,16 +120,23 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @RequiredArgsConstructor @Slf4j public class DeviceController extends BaseController { - private final DeviceBulkImportService deviceBulkImportService; - private static final String DEVICE_ID = "deviceId"; - private static final String DEVICE_NAME = "deviceName"; - private static final String TENANT_ID = "tenantId"; + protected static final String DEVICE_ID = "deviceId"; + protected static final String DEVICE_NAME = "deviceName"; + protected static final String TENANT_ID = "tenantId"; + + private final DeviceBulkImportService deviceBulkImportService; + @ApiOperation(value = "Get Device (getDeviceById)", + notes = "Fetch the Device object based on the provided Device Id. " + + "If the user has the authority of 'TENANT_ADMIN', 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) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.GET) @ResponseBody - public Device getDeviceById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + public Device getDeviceById(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); @@ -110,10 +146,16 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Device Info (getDeviceInfoById)", + notes = "Fetch the Device Info object based on the provided Device Id. " + + "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. " + + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/info/{deviceId}", method = RequestMethod.GET) @ResponseBody - public DeviceInfo getDeviceInfoById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + public DeviceInfo getDeviceInfoById(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); @@ -123,11 +165,20 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Create Or Update Device (saveDevice)", + notes = "Create or update the Device. When creating device, platform generates Device Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "Device credentials are also generated if not provided in the 'accessToken' request parameter. " + + "The newly created device id will be present in the response. " + + "Specify existing Device id to update the device. " + + "Referencing non-existing device Id will cause 'Not Found' error." + + "\n\nDevice name is unique in the scope of tenant. Use unique identifiers like MAC or IMEI for the device names and non-unique 'label' field for user-friendly visualization purposes." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device", method = RequestMethod.POST) @ResponseBody - public Device saveDevice(@RequestBody Device device, - @RequestParam(name = "accessToken", required = false) String accessToken) throws ThingsboardException { + public Device saveDevice(@ApiParam(value = "A JSON value representing the device.") @RequestBody Device device, + @ApiParam(value = "Optional value of the device credentials to be used during device creation. " + + "If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws ThingsboardException { boolean created = device.getId() == null; try { device.setTenantId(getCurrentUser().getTenantId()); @@ -141,7 +192,7 @@ public class DeviceController extends BaseController { Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken)); - onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created); + onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created, getCurrentUser()); return savedDevice; } catch (Exception e) { @@ -149,14 +200,45 @@ public class DeviceController extends BaseController { null, created ? ActionType.ADDED : ActionType.UPDATED, e); throw handleException(e); } + } + @ApiOperation(value = "Create Device (saveDevice) with credentials ", + notes = "Create or update the Device. When creating device, platform generates Device Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " + + "You may find the example of LwM2M device and RPK credentials below: \n\n" + + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST) + @ResponseBody + public Device saveDeviceWithCredentials(@ApiParam(value = "The JSON object with device and credentials. See method description above for example.") + @RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException { + Device device = checkNotNull(deviceAndCredentials.getDevice()); + DeviceCredentials credentials = checkNotNull(deviceAndCredentials.getCredentials()); + boolean created = device.getId() == null; + try { + device.setTenantId(getCurrentUser().getTenantId()); + checkEntity(device.getId(), device, Resource.DEVICE); + Device savedDevice = deviceService.saveDeviceWithCredentials(device, credentials); + checkNotNull(savedDevice); + tbClusterService.onDeviceUpdated(savedDevice, device); + logEntityAction(savedDevice.getId(), savedDevice, + savedDevice.getCustomerId(), + device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); + + return savedDevice; + } catch (Exception e) { + logEntityAction(emptyId(EntityType.DEVICE), device, + null, created ? ActionType.ADDED : ActionType.UPDATED, e); + throw handleException(e); + } } - private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated) { + private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated, SecurityUser user) { tbClusterService.onDeviceUpdated(savedDevice, oldDevice); try { - logEntityAction(savedDevice.getId(), savedDevice, + logEntityAction(user, savedDevice.getId(), savedDevice, savedDevice.getCustomerId(), updated ? ActionType.UPDATED : ActionType.ADDED, null); } catch (ThingsboardException e) { @@ -164,10 +246,13 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Delete device (deleteDevice)", + notes = "Deletes the device, it's credentials and all the relations (from and to the device). Referencing non-existing device Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteDevice(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + public void deleteDevice(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); @@ -193,10 +278,14 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Assign device to customer (assignDeviceToCustomer)", + notes = "Creates assignment of the device to customer. Customer will be able to query device afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST) @ResponseBody - public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId, + public Device assignDeviceToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable("customerId") String strCustomerId, + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(DEVICE_ID, strDeviceId); @@ -225,10 +314,13 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Unassign device from customer (unassignDeviceFromCustomer)", + notes = "Clears assignment of the device to customer. Customer will not be able to query device afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE) @ResponseBody - public Device unassignDeviceFromCustomer(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + public Device unassignDeviceFromCustomer(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); @@ -256,10 +348,15 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Make device publicly available (assignDeviceToPublicCustomer)", + notes = "Device will be available for non-authorized (not logged-in) users. " + + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " + + "However, users that are logged-in and belong to different tenant will not be able to access the device." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST) @ResponseBody - public Device assignDeviceToPublicCustomer(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + public Device assignDeviceToPublicCustomer(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); @@ -280,10 +377,13 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Device Credentials (getDeviceCredentialsByDeviceId)", + notes = "If during device creation there wasn't specified any credentials, platform generates random 'ACCESS_TOKEN' credentials." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET) @ResponseBody - public DeviceCredentials getDeviceCredentialsByDeviceId(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { + public DeviceCredentials getDeviceCredentialsByDeviceId(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); @@ -301,10 +401,16 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " + + "Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value. " + + "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device. " + + "The structure of device credentials id and value is simple for the 'ACCESS_TOKEN' but is much more complex for the 'MQTT_BASIC' or 'LWM2M_CREDENTIALS'." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/device/credentials", method = RequestMethod.POST) @ResponseBody - public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException { + public DeviceCredentials updateDeviceCredentials( + @ApiParam(value = "A JSON value representing the device credentials.") + @RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException { checkNotNull(deviceCredentials); try { Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS); @@ -325,15 +431,24 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Tenant Devices (getTenantDevices)", + notes = "Returns a page of devices owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantDevices( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -348,17 +463,28 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Tenant Device Infos (getTenantDeviceInfos)", + notes = "Returns a page of devices info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantDeviceInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(required = false) String deviceProfileId, + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder + ) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); @@ -375,10 +501,14 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Tenant Device (getTenantDevice)", + notes = "Requested device must be owned by tenant that the user belongs to. " + + "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET) @ResponseBody public Device getTenantDevice( + @ApiParam(value = DEVICE_NAME_DESCRIPTION) @RequestParam String deviceName) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -388,16 +518,26 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Customer Devices (getCustomerDevices)", + notes = "Returns a page of devices objects assigned to customer. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerDevices( - @PathVariable("customerId") String strCustomerId, + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -415,17 +555,28 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Customer Device Infos (getCustomerDeviceInfos)", + notes = "Returns a page of devices info objects assigned to customer. " + + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerDeviceInfos( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(required = false) String deviceProfileId, + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -446,10 +597,13 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)", + notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET) @ResponseBody public List getDevicesByIds( + @ApiParam(value = "A list of devices ids, separated by comma ','") @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException { checkArrayParameter("deviceIds", strDeviceIds); try { @@ -472,10 +626,16 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Find related devices (findByQuery)", + notes = "Returns all devices that are related to the specific entity. " + + "The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " + + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException { + public List findByQuery( + @ApiParam(value = "The device search query JSON") + @RequestBody DeviceSearchQuery query) throws ThingsboardException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getDeviceTypes()); @@ -496,6 +656,9 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get Device Types (getDeviceTypes)", + notes = "Returns a set of unique device profile names based on devices that are either owned by the tenant or assigned to the customer which user is performing the request." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/device/types", method = RequestMethod.GET) @ResponseBody @@ -510,10 +673,19 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Claim device (claimDevice)", + notes = "Claiming makes it possible to assign a device to the specific customer using device/server side claiming data (in the form of secret key)." + + "To make this happen you have to provide unique device name and optional claiming data (it is needed only for device-side claiming)." + + "Once device is claimed, the customer becomes its owner and customer users may access device data as well as control the device. \n" + + "In order to enable claiming devices feature a system parameter security.claim.allowClaimingByDefault should be set to true, " + + "otherwise a server-side claimingAllowed attribute with the value true is obligatory for provisioned devices. \n" + + "See official documentation for more details regarding claiming." + CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('CUSTOMER_USER')") @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.POST) @ResponseBody - public DeferredResult claimDevice(@PathVariable(DEVICE_NAME) String deviceName, + public DeferredResult claimDevice(@ApiParam(value = "Unique name of the device which is going to be claimed") + @PathVariable(DEVICE_NAME) String deviceName, + @ApiParam(value = "Claiming request which can optionally contain secret key") @RequestBody(required = false) ClaimRequest claimRequest) throws ThingsboardException { checkParameter(DEVICE_NAME, deviceName); try { @@ -564,10 +736,14 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Reclaim device (reClaimDevice)", + notes = "Reclaiming means the device will be unassigned from the customer and the device will be available for claiming again." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public DeferredResult reClaimDevice(@PathVariable(DEVICE_NAME) String deviceName) throws ThingsboardException { + public DeferredResult reClaimDevice(@ApiParam(value = "Unique name of the device which is going to be reclaimed") + @PathVariable(DEVICE_NAME) String deviceName) throws ThingsboardException { checkParameter(DEVICE_NAME, deviceName); try { final DeferredResult deferredResult = new DeferredResult<>(); @@ -615,10 +791,14 @@ public class DeviceController extends BaseController { return DataConstants.DEFAULT_SECRET_KEY; } + @ApiOperation(value = "Assign device to tenant (assignDeviceToTenant)", + notes = "Creates assignment of the device to tenant. Thereafter tenant will be able to reassign the device to a customer." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}/device/{deviceId}", method = RequestMethod.POST) @ResponseBody - public Device assignDeviceToTenant(@PathVariable(TENANT_ID) String strTenantId, + public Device assignDeviceToTenant(@ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) + @PathVariable(TENANT_ID) String strTenantId, + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(TENANT_ID, strTenantId); checkParameter(DEVICE_ID, strDeviceId); @@ -665,10 +845,19 @@ public class DeviceController extends BaseController { return metaData; } + @ApiOperation(value = "Assign device to edge (assignDeviceToEdge)", + notes = "Creates assignment of an existing device to an instance of The Edge. " + + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive a copy of assignment device " + + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once device will be delivered to edge service, it's going to be available for usage on remote edge instance." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.POST) @ResponseBody - public Device assignDeviceToEdge(@PathVariable(EDGE_ID) String strEdgeId, + public Device assignDeviceToEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) + @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(DEVICE_ID, strDeviceId); @@ -699,10 +888,19 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Unassign device from edge (unassignDeviceFromEdge)", + notes = "Clears assignment of the device to the edge. " + + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive an 'unassign' command to remove device " + + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once 'unassign' command will be delivered to edge service, it's going to remove device locally." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.DELETE) @ResponseBody - public Device unassignDeviceFromEdge(@PathVariable(EDGE_ID) String strEdgeId, + public Device unassignDeviceFromEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) + @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter(DEVICE_ID, strDeviceId); @@ -733,18 +931,30 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Get devices assigned to edge (getEdgeDevices)", + notes = "Returns a page of devices assigned to edge. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getEdgeDevices( + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_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 = "Timestamp. Devices with creation time before it won't be queried") @RequestParam(required = false) Long startTime, + @ApiParam(value = "Timestamp. Devices with creation time after it won't be queried") @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { @@ -776,10 +986,17 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Count devices by device profile (countByDeviceProfileAndEmptyOtaPackage)", + notes = "The platform gives an ability to load OTA (over-the-air) packages to devices. " + + "It can be done in two different ways: device scope or device profile scope." + + "In the response you will find the number of devices with specified device profile, but without previously defined device scope OTA package. " + + "It can be useful when you want to define number of devices that will be affected with future OTA package" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET) @ResponseBody - public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType, + public Long countByDeviceProfileAndEmptyOtaPackage(@ApiParam(value = "OTA package type", allowableValues = "FIRMWARE, SOFTWARE") + @PathVariable("otaPackageType") String otaPackageType, + @ApiParam(value = "Device Profile Id. I.g. '784f394c-42b6-435a-983c-b7beff2784f9'") @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException { checkParameter("OtaPackageType", otaPackageType); checkParameter("DeviceProfileId", deviceProfileId); @@ -793,11 +1010,14 @@ public class DeviceController extends BaseController { } } + @ApiOperation(value = "Import the bulk of devices (processDevicesBulkImport)", + notes = "There's an ability to import the bulk of devices using the only .csv file." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PostMapping("/device/bulk_import") public BulkImportResult processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception { - return deviceBulkImportService.processBulkImport(request, getCurrentUser(), importedDeviceInfo -> { - onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated()); + SecurityUser user = getCurrentUser(); + return deviceBulkImportService.processBulkImport(request, user, importedDeviceInfo -> { + onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated(), user); }); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index 4f74676be5..ccd3237e18 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -47,21 +49,43 @@ import java.util.List; import java.util.Objects; import java.util.UUID; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TRANSPORT_TYPE_ALLOWABLE_VALUES; + @RestController @TbCoreComponent @RequestMapping("/api") @Slf4j public class DeviceProfileController extends BaseController { - private static final String DEVICE_PROFILE_ID = "deviceProfileId"; @Autowired private TimeseriesService timeseriesService; + @ApiOperation(value = "Get Device Profile (getDeviceProfileById)", + notes = "Fetch the Device Profile object based on the provided Device Profile Id. " + + "The server checks that the device profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET) @ResponseBody - public DeviceProfile getDeviceProfileById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { + public DeviceProfile getDeviceProfileById( + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); try { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); @@ -71,10 +95,16 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Get Device Profile Info (getDeviceProfileInfoById)", + notes = "Fetch the Device Profile Info object based on the provided Device Profile Id. " + + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET) @ResponseBody - public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { + public DeviceProfileInfo getDeviceProfileInfoById( + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); try { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); @@ -84,6 +114,10 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Get Default Device Profile (getDefaultDeviceProfileInfo)", + notes = "Fetch the Default Device Profile Info object. " + + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/deviceProfileInfo/default", method = RequestMethod.GET) @ResponseBody @@ -95,10 +129,18 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)", + notes = "Get a set of unique time-series keys used by devices that belong to specified profile. " + + "If profile is not set returns a list of unique keys among all profiles. " + + "The call is used for auto-complete in the UI forms. " + + "The implementation limits the number of devices that participate in search to 100 as a trade of between accurate results and time-consuming queries. " + + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET) @ResponseBody public List getTimeseriesKeys( + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException { DeviceProfileId deviceProfileId; if (StringUtils.isNotEmpty(deviceProfileIdStr)) { @@ -115,10 +157,18 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Get attribute keys (getAttributesKeys)", + notes = "Get a set of unique attribute keys used by devices that belong to specified profile. " + + "If profile is not set returns a list of unique keys among all profiles. " + + "The call is used for auto-complete in the UI forms. " + + "The implementation limits the number of devices that participate in search to 100 as a trade of between accurate results and time-consuming queries. " + + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/devices/keys/attributes", method = RequestMethod.GET) @ResponseBody public List getAttributesKeys( + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException { DeviceProfileId deviceProfileId; if (StringUtils.isNotEmpty(deviceProfileIdStr)) { @@ -135,10 +185,21 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Create Or Update Device Profile (saveDeviceProfile)", + notes = "Create or update the Device Profile. When creating device profile, platform generates device profile id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created device profile id will be present in the response. " + + "Specify existing device profile id to update the device profile. " + + "Referencing non-existing device profile Id will cause 'Not Found' error. " + NEW_LINE + + "Device profile name is unique in the scope of tenant. Only one 'default' device profile may exist in scope of tenant." + DEVICE_PROFILE_DATA + + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json", + consumes = "application/json") @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST) @ResponseBody - public DeviceProfile saveDeviceProfile(@RequestBody DeviceProfile deviceProfile) throws ThingsboardException { + public DeviceProfile saveDeviceProfile( + @ApiParam(value = "A JSON value representing the device profile.") + @RequestBody DeviceProfile deviceProfile) throws ThingsboardException { try { boolean created = deviceProfile.getId() == null; deviceProfile.setTenantId(getTenantId()); @@ -179,10 +240,16 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Delete device profile (deleteDeviceProfile)", + notes = "Deletes the device profile. Referencing non-existing device profile Id will cause an error. " + + "Can't delete the device profile if it is referenced by existing devices." + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { + public void deleteDeviceProfile( + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); try { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); @@ -206,10 +273,15 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Make Device Profile Default (setDefaultDeviceProfile)", + notes = "Marks device profile as default within a tenant scope." + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST) @ResponseBody - public DeviceProfile setDefaultDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { + public DeviceProfile setDefaultDeviceProfile( + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException { checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId); try { DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId)); @@ -237,14 +309,24 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Get Device Profiles (getDeviceProfiles)", + notes = "Returns a page of devices profile objects owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getDeviceProfiles(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getDeviceProfiles( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink)); @@ -253,15 +335,26 @@ public class DeviceProfileController extends BaseController { } } + @ApiOperation(value = "Get Device Profiles for transport type (getDeviceProfileInfos)", + notes = "Returns a page of devices profile info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getDeviceProfileInfos(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder, - @RequestParam(required = false) String transportType) throws ThingsboardException { + public PageData getDeviceProfileInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_PROFILE_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 = "Type of the transport", allowableValues = TRANSPORT_TYPE_ALLOWABLE_VALUES) + @RequestParam(required = false) String transportType) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType)); 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 21c39cdd58..219041e82c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -17,9 +17,12 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -66,6 +69,22 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_TYPE_DESCRIPTION; +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.RULE_CHAIN_ID_PARAM_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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @Slf4j @@ -75,7 +94,11 @@ public class EdgeController extends BaseController { private final EdgeBulkImportService edgeBulkImportService; 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. " + + "If the user has the authority of 'Customer User', the server checks that the edge is assigned to the same customer."; + @ApiOperation(value = "Is edges support enabled (isEdgesSupportEnabled)", + notes = "Returns 'true' if edges support enabled on server, 'false' - otherwise.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edges/enabled", method = RequestMethod.GET) @ResponseBody @@ -83,10 +106,14 @@ public class EdgeController extends BaseController { return edgesEnabled; } + @ApiOperation(value = "Get Edge (getEdgeById)", + notes = "Get the Edge object based on the provided Edge Id. " + EDGE_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.GET) @ResponseBody - public Edge getEdgeById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + public Edge getEdgeById(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); @@ -100,10 +127,14 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Edge Info (getEdgeInfoById)", + notes = "Get the Edge Info object based on the provided Edge Id. " + EDGE_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/info/{edgeId}", method = RequestMethod.GET) @ResponseBody - public EdgeInfo getEdgeInfoById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + public EdgeInfo getEdgeInfoById(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); @@ -117,10 +148,19 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Create Or Update Edge (saveEdge)", + notes = "Create or update the Edge. When creating edge, platform generates Edge Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created edge id will be present in the response. " + + "Specify existing Edge id to update the edge. " + + "Referencing non-existing Edge Id will cause 'Not Found' error." + + "\n\nEdge name is unique in the scope of tenant. Use unique identifiers like MAC or IMEI for the edge names and non-unique 'label' field for user-friendly visualization purposes." + + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge", method = RequestMethod.POST) @ResponseBody - public Edge saveEdge(@RequestBody Edge edge) throws ThingsboardException { + public Edge saveEdge(@ApiParam(value = "A JSON value representing the edge.", required = true) + @RequestBody Edge edge) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); edge.setTenantId(tenantId); @@ -140,7 +180,7 @@ public class EdgeController extends BaseController { edge.getId(), edge); Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true)); - onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created); + onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created, getCurrentUser()); return savedEdge; } catch (Exception e) { @@ -150,7 +190,7 @@ public class EdgeController extends BaseController { } } - private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated) throws IOException, ThingsboardException { + private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated, SecurityUser user) throws IOException, ThingsboardException { if (!updated) { ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edge.getId()); edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, edgeTemplateRootRuleChain.getId()); @@ -160,13 +200,16 @@ public class EdgeController extends BaseController { tbClusterService.broadcastEntityStateChangeEvent(edge.getTenantId(), edge.getId(), updated ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED); - logEntityAction(edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null); + logEntityAction(user, edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null); } + @ApiOperation(value = "Delete edge (deleteEdge)", + notes = "Deletes the edge. Referencing non-existing edge Id will cause an error."+ TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteEdge(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + public void deleteEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); @@ -191,13 +234,21 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Tenant Edges (getEdges)", + notes = "Returns a page of edges owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getEdges(@RequestParam int pageSize, + public PageData getEdges(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); @@ -208,10 +259,15 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Assign edge to customer (assignEdgeToCustomer)", + notes = "Creates assignment of the edge to customer. Customer will be able to query edge afterwards." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/edge/{edgeId}", method = RequestMethod.POST) @ResponseBody - public Edge assignEdgeToCustomer(@PathVariable("customerId") String strCustomerId, + public Edge assignEdgeToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("customerId") String strCustomerId, + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter("customerId", strCustomerId); checkParameter(EDGE_ID, strEdgeId); @@ -243,10 +299,14 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Unassign edge from customer (unassignEdgeFromCustomer)", + notes = "Clears assignment of the edge to customer. Customer will not be able to query edge afterwards." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/edge/{edgeId}", method = RequestMethod.DELETE) @ResponseBody - public Edge unassignEdgeFromCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + public Edge unassignEdgeFromCustomer(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); @@ -277,10 +337,16 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Make edge publicly available (assignEdgeToPublicCustomer)", + notes = "Edge will be available for non-authorized (not logged-in) users. " + + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " + + "However, users that are logged-in and belong to different tenant will not be able to access the edge." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/edge/{edgeId}", method = RequestMethod.POST) @ResponseBody - public Edge assignEdgeToPublicCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { + public Edge assignEdgeToPublicCustomer(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); @@ -304,15 +370,24 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Tenant Edges (getTenantEdges)", + notes = "Returns a page of edges owned by tenant. " + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantEdges( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EDGE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -327,15 +402,25 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Tenant Edge Infos (getTenantEdgeInfos)", + notes = "Returns a page of edges info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantEdgeInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EDGE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -350,10 +435,15 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Tenant Edge (getTenantEdge)", + notes = "Requested edge must be owned by tenant or customer that the user belongs to. " + + "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/edges", params = {"edgeName"}, method = RequestMethod.GET) @ResponseBody - public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException { + public Edge getTenantEdge(@ApiParam(value = "Unique name of the edge", required = true) + @RequestParam String edgeName) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName)); @@ -362,10 +452,16 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Set root rule chain for provided edge (setRootRuleChain)", + notes = "Change root rule chain of the edge to the new provided rule chain. \n" + + "This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/{ruleChainId}/root", method = RequestMethod.POST) @ResponseBody - public Edge setRootRuleChain(@PathVariable(EDGE_ID) String strEdgeId, + public Edge setRootRuleChain(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION, required = true) @PathVariable("ruleChainId") String strRuleChainId) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); checkParameter("ruleChainId", strRuleChainId); @@ -394,16 +490,26 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Customer Edges (getCustomerEdges)", + notes = "Returns a page of edges objects assigned to customer. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerEdges( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EDGE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -429,16 +535,26 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Customer Edge Infos (getCustomerEdgeInfos)", + notes = "Returns a page of edges info objects assigned to customer. " + + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerEdgeInfos( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EDGE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -464,10 +580,14 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Edges By Ids (getEdgesByIds)", + notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET) @ResponseBody public List getEdgesByIds( + @ApiParam(value = "A list of edges ids, separated by comma ','", required = true) @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException { checkArrayParameter("edgeIds", strEdgeIds); try { @@ -496,6 +616,11 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Find related edges (findByQuery)", + notes = "Returns all edges that are related to the specific entity. " + + "The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " + + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edges", method = RequestMethod.POST) @ResponseBody @@ -527,6 +652,10 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Get Edge Types (getEdgeTypes)", + notes = "Returns a set of unique edge types based on edges that are either owned by the tenant or assigned to the customer which user is performing the request." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/types", method = RequestMethod.GET) @ResponseBody @@ -541,9 +670,13 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Sync edge (syncEdge)", + notes = "Starts synchronization process between edge and cloud. \n" + + "All entities that are assigned to particular edge are going to be send to remote edge service." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/sync/{edgeId}", method = RequestMethod.POST) - public void syncEdge(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + public void syncEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { checkParameter("edgeId", strEdgeId); try { if (isEdgesEnabled()) { @@ -560,10 +693,13 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Find missing rule chains (findMissingToRelatedRuleChains)", + notes = "Returns list of rule chains ids that are not assigned to particular edge, but these rule chains are present in the already assigned rule chains to edge." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/missingToRelatedRuleChains/{edgeId}", method = RequestMethod.GET) @ResponseBody - public String findMissingToRelatedRuleChains(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException { + public String findMissingToRelatedRuleChains(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("edgeId") String strEdgeId) throws ThingsboardException { try { EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); edgeId = checkNotNull(edgeId); @@ -575,9 +711,12 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Import the bulk of edges (processEdgesBulkImport)", + notes = "There's an ability to import the bulk of edges using the only .csv file." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @PostMapping("/edge/bulk_import") - public BulkImportResult processEdgeBulkImport(@RequestBody BulkImportRequest request) throws Exception { + public BulkImportResult processEdgesBulkImport(@RequestBody BulkImportRequest request) throws Exception { SecurityUser user = getCurrentUser(); RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(user.getTenantId()); if (edgeTemplateRootRuleChain == null) { @@ -586,7 +725,7 @@ public class EdgeController extends BaseController { return edgeBulkImportService.processBulkImport(request, user, importedAssetInfo -> { try { - onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated()); + onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated(), user); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java index 22191efddf..72fbba0fe6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeEventController.java @@ -15,8 +15,11 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -34,6 +37,15 @@ import org.thingsboard.server.dao.edge.EdgeEventService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_SORT_PROPERTY_ALLOWABLE_VALUES; +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; + @Slf4j @RestController @TbCoreComponent @@ -45,17 +57,28 @@ public class EdgeEventController extends BaseController { public static final String EDGE_ID = "edgeId"; + @ApiOperation(value = "Get Edge Events (getEdgeEvents)", + notes = "Returns a page of edge events for the requested edge. " + + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/events", method = RequestMethod.GET) @ResponseBody public PageData getEdgeEvents( + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(EDGE_ID) String strEdgeId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = "The case insensitive 'startsWith' filter based on the edge event type name.") @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_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 = "Timestamp. Edge events with creation time before it won't be queried") @RequestParam(required = false) Long startTime, + @ApiParam(value = "Timestamp. Edge events with creation time after it won't be queried") @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter(EDGE_ID, strEdgeId); try { diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 673ac9646b..019066c636 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -37,6 +39,10 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; +import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -47,10 +53,13 @@ public class EntityQueryController extends BaseController { private static final int MAX_PAGE_SIZE = 100; + @ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST) @ResponseBody - public long countEntitiesByQuery(@RequestBody EntityCountQuery query) throws ThingsboardException { + public long countEntitiesByQuery( + @ApiParam(value = "A JSON value representing the entity count query. See API call notes above for more details.") + @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); try { return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); @@ -59,10 +68,13 @@ public class EntityQueryController extends BaseController { } } + @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entitiesQuery/find", method = RequestMethod.POST) @ResponseBody - public PageData findEntityDataByQuery(@RequestBody EntityDataQuery query) throws ThingsboardException { + public PageData findEntityDataByQuery( + @ApiParam(value = "A JSON value representing the entity data query. See API call notes above for more details.") + @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); try { return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); @@ -71,10 +83,13 @@ public class EntityQueryController extends BaseController { } } + @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarmsQuery/find", method = RequestMethod.POST) @ResponseBody - public PageData findAlarmDataByQuery(@RequestBody AlarmDataQuery query) throws ThingsboardException { + public PageData findAlarmDataByQuery( + @ApiParam(value = "A JSON value representing the alarm data query. See API call notes above for more details.") + @RequestBody AlarmDataQuery query) throws ThingsboardException { checkNotNull(query); try { return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); @@ -83,12 +98,18 @@ public class EntityQueryController extends BaseController { } } + @ApiOperation(value = "Find Entity Keys by Query", + notes = "Uses entity data query (see 'Find Entity Data by Query') to find first 100 entities. Then fetch and return all unique time-series and/or attribute keys. Used mostly for UI hints.") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entitiesQuery/find/keys", method = RequestMethod.POST) @ResponseBody - public DeferredResult findEntityTimeseriesAndAttributesKeysByQuery(@RequestBody EntityDataQuery query, - @RequestParam("timeseries") boolean isTimeseries, - @RequestParam("attributes") boolean isAttributes) throws ThingsboardException { + public DeferredResult findEntityTimeseriesAndAttributesKeysByQuery( + @ApiParam(value = "A JSON value representing the entity data query. See API call notes above for more details.") + @RequestBody EntityDataQuery query, + @ApiParam(value = "Include all unique time-series keys to the result.") + @RequestParam("timeseries") boolean isTimeseries, + @ApiParam(value = "Include all unique attribute keys to the result.") + @RequestParam("attributes") boolean isAttributes) throws ThingsboardException { TenantId tenantId = getTenantId(); checkNotNull(query); try { 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 3f072b77f0..f5c5bdd284 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -40,6 +43,11 @@ import org.thingsboard.server.service.security.permission.Operation; import java.util.List; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RELATION_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RELATION_TYPE_GROUP_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RELATION_TYPE_PARAM_DESCRIPTION; @RestController @TbCoreComponent @@ -52,10 +60,23 @@ public class EntityRelationController extends BaseController { public static final String RELATION_TYPE = "relationType"; public static final String TO_ID = "toId"; + private static final String SECURITY_CHECKS_ENTITIES_DESCRIPTION = "\n\nIf the user has the authority of 'System Administrator', the server checks that 'from' and 'to' entities are owned by the sysadmin. " + + "If the user has the authority of 'Tenant Administrator', the server checks that 'from' and 'to' entities are owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the 'from' and 'to' entities are assigned to the same customer."; + + private static final String SECURITY_CHECKS_ENTITY_DESCRIPTION = "\n\nIf the user has the authority of 'System Administrator', the server checks that the entity is owned by the sysadmin. " + + "If the user has the authority of 'Tenant Administrator', the server checks that the entity is owned by the same tenant. " + + "If the user has the authority of 'Customer User', the server checks that the entity is assigned to the same customer."; + + @ApiOperation(value = "Create Relation (saveRelation)", + notes = "Creates or updates a relation between two entities in the platform. " + + "Relations unique key is a combination of from/to entity id and relation type group and relation type. " + + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relation", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void saveRelation(@RequestBody EntityRelation relation) throws ThingsboardException { + public void saveRelation(@ApiParam(value = "A JSON value representing the relation.", required = true) + @RequestBody EntityRelation relation) throws ThingsboardException { try { checkNotNull(relation); checkEntityId(relation.getFrom(), Operation.WRITE); @@ -80,14 +101,17 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Delete Relation (deleteRelation)", + notes = "Deletes a relation between two entities in the platform. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relation", method = RequestMethod.DELETE, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) @ResponseStatus(value = HttpStatus.OK) - public void deleteRelation(@RequestParam(FROM_ID) String strFromId, - @RequestParam(FROM_TYPE) String strFromType, - @RequestParam(RELATION_TYPE) String strRelationType, - @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, - @RequestParam(TO_ID) String strToId, @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { + public void deleteRelation(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, + @ApiParam(value = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); checkParameter(RELATION_TYPE, strRelationType); @@ -119,11 +143,14 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Delete Relations (deleteRelations)", + notes = "Deletes all the relation (both 'from' and 'to' direction) for the specified entity. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"id", "type"}) + @RequestMapping(value = "/relations", method = RequestMethod.DELETE, params = {"entityId", "entityType"}) @ResponseStatus(value = HttpStatus.OK) - public void deleteRelations(@RequestParam("entityId") String strId, - @RequestParam("entityType") String strType) throws ThingsboardException { + public void deleteRelations(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam("entityId") String strId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam("entityType") String strType) throws ThingsboardException { checkParameter("entityId", strId); checkParameter("entityType", strType); EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId); @@ -137,14 +164,18 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get Relation (getRelation)", + notes = "Returns relation object between two specified entities if present. Otherwise throws exception. " + SECURITY_CHECKS_ENTITIES_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relation", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE, TO_ID, TO_TYPE}) @ResponseBody - public EntityRelation getRelation(@RequestParam(FROM_ID) String strFromId, - @RequestParam(FROM_TYPE) String strFromType, - @RequestParam(RELATION_TYPE) String strRelationType, - @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, - @RequestParam(TO_ID) String strToId, @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { + public EntityRelation getRelation(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, + @ApiParam(value = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType) throws ThingsboardException { try { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -162,11 +193,16 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get List of Relations (findByFrom)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE}) @ResponseBody - public List findByFrom(@RequestParam(FROM_ID) String strFromId, - @RequestParam(FROM_TYPE) String strFromType, + public List findByFrom(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -180,11 +216,16 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get List of Relation Infos (findInfoByFrom)", + notes = "Returns list of relation info objects for the specified entity by the 'from' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE}) @ResponseBody - public List findInfoByFrom(@RequestParam(FROM_ID) String strFromId, - @RequestParam(FROM_TYPE) String strFromType, + public List findInfoByFrom(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -198,12 +239,17 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get List of Relations (findByFrom)", + notes = "Returns list of relation objects for the specified entity by the 'from' direction and relation type. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {FROM_ID, FROM_TYPE, RELATION_TYPE}) @ResponseBody - public List findByFrom(@RequestParam(FROM_ID) String strFromId, - @RequestParam(FROM_TYPE) String strFromType, - @RequestParam(RELATION_TYPE) String strRelationType, + public List findByFrom(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_ID) String strFromId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(FROM_TYPE) String strFromType, + @ApiParam(value = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(FROM_ID, strFromId); checkParameter(FROM_TYPE, strFromType); @@ -218,11 +264,16 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get List of Relations (findByTo)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE}) @ResponseBody - public List findByTo(@RequestParam(TO_ID) String strToId, - @RequestParam(TO_TYPE) String strToType, + public List findByTo(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(TO_ID, strToId); checkParameter(TO_TYPE, strToType); @@ -236,11 +287,16 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get List of Relation Infos (findInfoByTo)", + notes = "Returns list of relation info objects for the specified entity by the 'to' direction. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION + " " + RELATION_INFO_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations/info", method = RequestMethod.GET, params = {TO_ID, TO_TYPE}) @ResponseBody - public List findInfoByTo(@RequestParam(TO_ID) String strToId, - @RequestParam(TO_TYPE) String strToType, + public List findInfoByTo(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(TO_ID, strToId); checkParameter(TO_TYPE, strToType); @@ -254,12 +310,17 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Get List of Relations (findByTo)", + notes = "Returns list of relation objects for the specified entity by the 'to' direction and relation type. " + + SECURITY_CHECKS_ENTITY_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.GET, params = {TO_ID, TO_TYPE, RELATION_TYPE}) @ResponseBody - public List findByTo(@RequestParam(TO_ID) String strToId, - @RequestParam(TO_TYPE) String strToType, - @RequestParam(RELATION_TYPE) String strRelationType, + public List findByTo(@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @RequestParam(TO_ID) String strToId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(TO_TYPE) String strToType, + @ApiParam(value = RELATION_TYPE_PARAM_DESCRIPTION, required = true) @RequestParam(RELATION_TYPE) String strRelationType, + @ApiParam(value = RELATION_TYPE_GROUP_PARAM_DESCRIPTION) @RequestParam(value = "relationTypeGroup", required = false) String strRelationTypeGroup) throws ThingsboardException { checkParameter(TO_ID, strToId); checkParameter(TO_TYPE, strToType); @@ -274,10 +335,15 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Find related entities (findByQuery)", + notes = "Returns all entities that are related to the specific entity. " + + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + + "See 'Model' tab of the Parameters for more info.", produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { + public List findByQuery(@ApiParam(value = "A JSON value representing the entity relations query object.", required = true) + @RequestBody EntityRelationsQuery query) throws ThingsboardException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); @@ -289,10 +355,15 @@ public class EntityRelationController extends BaseController { } } + @ApiOperation(value = "Find related entity infos (findInfoByQuery)", + notes = "Returns all entity infos that are related to the specific entity. " + + "The entity id, relation type, entity types, depth of the search, and other query parameters defined using complex 'EntityRelationsQuery' object. " + + "See 'Model' tab of the Parameters for more info. " + RELATION_INFO_DESCRIPTION, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/relations/info", method = RequestMethod.POST) @ResponseBody - public List findInfoByQuery(@RequestBody EntityRelationsQuery query) throws ThingsboardException { + public List findInfoByQuery(@ApiParam(value = "A JSON value representing the entity relations query object.", required = true) + @RequestBody EntityRelationsQuery query) throws ThingsboardException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getFilters()); diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java index 63c151b24c..906e0f4dd1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityViewController.java @@ -20,9 +20,12 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -38,7 +41,6 @@ import org.thingsboard.server.common.data.EntitySubtype; 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.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -49,7 +51,6 @@ import org.thingsboard.server.common.data.id.EdgeId; 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.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; @@ -74,7 +75,28 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.thingsboard.server.controller.CustomerController.CUSTOMER_ID; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE; +import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; /** @@ -91,10 +113,16 @@ public class EntityViewController extends BaseController { @Autowired private TimeseriesService tsService; + @ApiOperation(value = "Get entity view (getEntityViewById)", + notes = "Fetch the EntityView object based on the provided entity view id. " + + ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET) @ResponseBody - public EntityView getEntityViewById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + public EntityView getEntityViewById( + @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); try { return checkEntityViewId(new EntityViewId(toUUID(strEntityViewId)), Operation.READ); @@ -103,10 +131,16 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Entity View info (getEntityViewInfoById)", + notes = "Fetch the Entity View info object based on the provided Entity View Id. " + + ENTITY_VIEW_INFO_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView/info/{entityViewId}", method = RequestMethod.GET) @ResponseBody - public EntityViewInfo getEntityViewInfoById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + public EntityViewInfo getEntityViewInfoById( + @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); try { EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); @@ -116,10 +150,15 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Save or update entity view (saveEntityView)", + notes = ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView", method = RequestMethod.POST) @ResponseBody - public EntityView saveEntityView(@RequestBody EntityView entityView) throws ThingsboardException { + public EntityView saveEntityView( + @ApiParam(value = "A JSON object representing the entity view.") + @RequestBody EntityView entityView) throws ThingsboardException { try { entityView.setTenantId(getCurrentUser().getTenantId()); @@ -353,10 +392,15 @@ public class EntityViewController extends BaseController { keys); } + @ApiOperation(value = "Delete entity view (deleteEntityView)", + notes = "Delete the EntityView object based on the provided entity view id. " + + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteEntityView(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + public void deleteEntityView( + @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); try { EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); @@ -378,10 +422,14 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Entity View by name (getTenantEntityView)", + notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET) @ResponseBody public EntityView getTenantEntityView( + @ApiParam(value = "Entity View name") @RequestParam String entityViewName) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -391,11 +439,16 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)", + notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST) @ResponseBody - public EntityView assignEntityViewToCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId, - @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + public EntityView assignEntityViewToCustomer( + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(CUSTOMER_ID, strCustomerId); checkParameter(ENTITY_VIEW_ID, strEntityViewId); try { @@ -422,10 +475,14 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Unassign Entity View from customer (unassignEntityViewFromCustomer)", + notes = "Clears assignment of the Entity View to customer. Customer will not be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE) @ResponseBody - public EntityView unassignEntityViewFromCustomer(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + public EntityView unassignEntityViewFromCustomer( + @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); try { EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); @@ -451,18 +508,28 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Customer Entity Views (getCustomerEntityViews)", + notes = "Returns a page of Entity View objects assigned to customer. " + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerEntityViews( - @PathVariable("customerId") String strCustomerId, + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = ENTITY_VIEW_TYPE) @RequestParam(required = false) String type, + @ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { - checkParameter("customerId", strCustomerId); + checkParameter(CUSTOMER_ID, strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); @@ -478,16 +545,26 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Customer Entity View info (getCustomerEntityViewInfos)", + notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerEntityViewInfos( - @PathVariable("customerId") String strCustomerId, + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = ENTITY_VIEW_TYPE) @RequestParam(required = false) String type, + @ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -505,15 +582,24 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Tenant Entity Views (getTenantEntityViews)", + notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantEntityViews( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = ENTITY_VIEW_TYPE) @RequestParam(required = false) String type, + @ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -529,15 +615,24 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Tenant Entity Views (getTenantEntityViews)", + notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION + + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantEntityViewInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = ENTITY_VIEW_TYPE) @RequestParam(required = false) String type, + @ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -552,10 +647,16 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Find related entity views (findByQuery)", + notes = "Returns all entity views that are related to the specific entity. " + + "The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " + + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityViews", method = RequestMethod.POST) @ResponseBody - public List findByQuery(@RequestBody EntityViewSearchQuery query) throws ThingsboardException { + public List findByQuery( + @ApiParam(value = "The entity view search query JSON") + @RequestBody EntityViewSearchQuery query) throws ThingsboardException { checkNotNull(query); checkNotNull(query.getParameters()); checkNotNull(query.getEntityViewTypes()); @@ -576,6 +677,9 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Get Entity View Types (getEntityViewTypes)", + notes = "Returns a set of unique entity view types based on entity views that are either owned by the tenant or assigned to the customer which user is performing the request." + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/entityView/types", method = RequestMethod.GET) @ResponseBody @@ -590,10 +694,16 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Make entity view publicly available (assignEntityViewToPublicCustomer)", + notes = "Entity View will be available for non-authorized (not logged-in) users. " + + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " + + "However, users that are logged-in and belong to different tenant will not be able to access the entity view." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/entityView/{entityViewId}", method = RequestMethod.POST) @ResponseBody - public EntityView assignEntityViewToPublicCustomer(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { + public EntityView assignEntityViewToPublicCustomer( + @ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION) + @PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException { checkParameter(ENTITY_VIEW_ID, strEntityViewId); try { EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId)); @@ -614,6 +724,13 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Assign entity view to edge (assignEntityViewToEdge)", + notes = "Creates assignment of an existing entity view to an instance of The Edge. " + + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive a copy of assignment entity view " + + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once entity view will be delivered to edge service, it's going to be available for usage on remote edge instance.", + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.POST) @ResponseBody @@ -644,6 +761,13 @@ public class EntityViewController extends BaseController { } } + @ApiOperation(value = "Unassign entity view from edge (unassignEntityViewFromEdge)", + notes = "Clears assignment of the entity view to the edge. " + + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive an 'unassign' command to remove entity view " + + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once 'unassign' command will be delivered to edge service, it's going to remove entity view locally.", + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/entityView/{entityViewId}", method = RequestMethod.DELETE) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/EventController.java b/application/src/main/java/org/thingsboard/server/controller/EventController.java index 7e49ba55d4..d1e9ad8efc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EventController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EventController.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -37,28 +40,96 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_DEBUG_RULE_NODE_FILTER_OBJ; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_END_TIME_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_ERROR_FILTER_OBJ; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_LC_EVENT_FILTER_OBJ; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_START_TIME_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_STATS_FILTER_OBJ; +import static org.thingsboard.server.controller.ControllerConstants.EVENT_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE; +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.TENANT_ID; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; + @RestController @TbCoreComponent @RequestMapping("/api") public class EventController extends BaseController { + private static final String EVENT_FILTER_DEFINITION = "# Event Filter Definition" + NEW_LINE + + "5 different eventFilter objects could be set for different event types. " + + "The eventType field is required. Others are optional. If some of them are set, the filtering will be applied according to them. " + + "See the examples below for all the fields used for each event type filtering. " + NEW_LINE + + "Note," + NEW_LINE + + " * 'server' - string value representing the server name, identifier or ip address where the platform is running;\n" + + " * 'errorStr' - the case insensitive 'contains' filter based on error message." + NEW_LINE + + "## Error Event Filter" + NEW_LINE + + EVENT_ERROR_FILTER_OBJ + NEW_LINE + + " * 'method' - string value representing the method name when the error happened." + NEW_LINE + + "## Lifecycle Event Filter" + NEW_LINE + + EVENT_LC_EVENT_FILTER_OBJ + NEW_LINE + + " * 'event' - string value representing the lifecycle event type;\n" + + " * 'status' - string value representing status of the lifecycle event." + NEW_LINE + + "## Statistics Event Filter" + NEW_LINE + + EVENT_STATS_FILTER_OBJ + NEW_LINE + + " * 'messagesProcessed' - the minimum number of successfully processed messages;\n" + + " * 'errorsOccurred' - the minimum number of errors occurred during messages processing." + NEW_LINE + + "## Debug Rule Node Event Filter" + NEW_LINE + + EVENT_DEBUG_RULE_NODE_FILTER_OBJ + NEW_LINE + + "## Debug Rule Chain Event Filter" + NEW_LINE + + EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ + NEW_LINE + + " * 'msgDirectionType' - string value representing msg direction type (incoming to entity or outcoming from entity);\n" + + " * 'dataSearch' - the case insensitive 'contains' filter based on data (key and value) for the message;\n" + + " * 'metadataSearch' - the case insensitive 'contains' filter based on metadata (key and value) for the message;\n" + + " * 'entityName' - string value representing the entity type;\n" + + " * 'relationType' - string value representing the type of message routing;\n" + + " * 'entityId' - string value representing the entity id in the event body (originator of the message);\n" + + " * 'msgType' - string value representing the message type;\n" + + " * 'isError' - boolean value to filter the errors." + NEW_LINE; + @Autowired private EventService eventService; + @ApiOperation(value = "Get Events by type (getEvents)", + notes = "Returns a page of events for specified entity by specifying event type. " + + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}/{eventType}", method = RequestMethod.GET) @ResponseBody public PageData getEvents( - @PathVariable("entityType") String strEntityType, - @PathVariable("entityId") String strEntityId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = "A string value representing event type", example = "STATS", required = true) @PathVariable("eventType") String eventType, - @RequestParam("tenantId") String strTenantId, + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION, required = true) + @RequestParam(TENANT_ID) String strTenantId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EVENT_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EVENT_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 = EVENT_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = EVENT_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -74,19 +145,32 @@ public class EventController extends BaseController { } } + @ApiOperation(value = "Get Events (getEvents)", + notes = "Returns a page of events for specified entity. " + + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.GET) @ResponseBody public PageData getEvents( - @PathVariable("entityType") String strEntityType, - @PathVariable("entityId") String strEntityId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION, required = true) @RequestParam("tenantId") String strTenantId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = EVENT_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EVENT_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 = EVENT_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = EVENT_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -104,20 +188,36 @@ public class EventController extends BaseController { } } + @ApiOperation(value = "Get Events by event filter (getEvents)", + notes = "Returns a page of events for the chosen entity by specifying the event filter. " + + PAGE_DATA_PARAMETERS + NEW_LINE + + EVENT_FILTER_DEFINITION, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/events/{entityType}/{entityId}", method = RequestMethod.POST) @ResponseBody public PageData getEvents( - @PathVariable("entityType") String strEntityType, - @PathVariable("entityId") String strEntityId, - @RequestParam("tenantId") String strTenantId, + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_TYPE) String strEntityType, + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(ENTITY_ID) String strEntityId, + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION, required = true) + @RequestParam(TENANT_ID) String strTenantId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = "A JSON value representing the event filter.", required = true) @RequestBody EventFilter eventFilter, + @ApiParam(value = EVENT_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EVENT_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 = EVENT_START_TIME_DESCRIPTION) @RequestParam(required = false) Long startTime, + @ApiParam(value = EVENT_END_TIME_DESCRIPTION) @RequestParam(required = false) Long endTime) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -127,7 +227,7 @@ public class EventController extends BaseController { EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); checkEntityId(entityId, Operation.READ); - if(sortProperty != null && sortProperty.equals("createdTime") && eventFilter.hasFilterForJsonBody()) { + if (sortProperty != null && sortProperty.equals("createdTime") && eventFilter.hasFilterForJsonBody()) { sortProperty = ModelConstants.CREATED_TIME_PROPERTY; } diff --git a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java index 6c1a7f315d..7a772a69cc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; @@ -26,8 +28,7 @@ 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.EntityType; -import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -38,6 +39,9 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.Map; +import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @Slf4j @RestController @TbCoreComponent @@ -45,12 +49,23 @@ import java.util.Map; public class Lwm2mController extends BaseController { @Autowired - protected LwM2MService lwM2MService; + private DeviceController deviceController; + + @Autowired + private LwM2MService lwM2MService; + public static final String IS_BOOTSTRAP_SERVER = "isBootstrapServer"; + + @ApiOperation(value = "Get Lwm2m Bootstrap SecurityInfo (getLwm2mBootstrapSecurityInfo)", + notes = "Get the Lwm2m Bootstrap SecurityInfo object (of the current server) based on the provided isBootstrapServer parameter. If isBootstrapServer == true, get the parameters of the current Bootstrap Server. If isBootstrapServer == false, get the parameters of the current Lwm2m Server. Used for client settings when starting the client in Bootstrap mode. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{isBootstrapServer}", method = RequestMethod.GET) @ResponseBody - public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("isBootstrapServer") boolean bootstrapServer) throws ThingsboardException { + public ServerSecurityConfig getLwm2mBootstrapSecurityInfo( + @ApiParam(value = IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION) + @PathVariable(IS_BOOTSTRAP_SERVER) boolean bootstrapServer) throws ThingsboardException { try { return lwM2MService.getServerSecurityInfo(bootstrapServer); } catch (Exception e) { @@ -58,28 +73,14 @@ public class Lwm2mController extends BaseController { } } + @ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST) @ResponseBody - public Device saveDeviceWithCredentials(@RequestBody (required=false) Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { + public Device saveDeviceWithCredentials(@RequestBody Map, Object> deviceWithDeviceCredentials) throws ThingsboardException { ObjectMapper mapper = new ObjectMapper(); Device device = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class)); - DeviceCredentials credentials = checkNotNull(mapper.convertValue( deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); - try { - device.setTenantId(getCurrentUser().getTenantId()); - checkEntity(device.getId(), device, Resource.DEVICE); - Device savedDevice = deviceService.saveDeviceWithCredentials(device, credentials); - checkNotNull(savedDevice); - tbClusterService.onDeviceUpdated(savedDevice, device); - logEntityAction(savedDevice.getId(), savedDevice, - savedDevice.getCustomerId(), - device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); - - return savedDevice; - } catch (Exception e) { - logEntityAction(emptyId(EntityType.DEVICE), device, - null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e); - throw handleException(e); - } + DeviceCredentials credentials = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class)); + return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials)); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java index 434aaf719f..816b8fc849 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2ConfigTemplateController.java @@ -15,12 +15,18 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.audit.ActionType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; @@ -30,6 +36,9 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api/oauth2/config/template") @@ -37,6 +46,10 @@ import java.util.List; public class OAuth2ConfigTemplateController extends BaseController { private static final String CLIENT_REGISTRATION_TEMPLATE_ID = "clientRegistrationTemplateId"; + private static final String OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION = "Client registration template is OAuth2 provider configuration template with default settings for registering new OAuth2 clients"; + + @ApiOperation(value = "Create or update OAuth2 client registration template (saveClientRegistrationTemplate)" + SYSTEM_AUTHORITY_PARAGRAPH, + notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @@ -49,10 +62,13 @@ public class OAuth2ConfigTemplateController extends BaseController { } } + @ApiOperation(value = "Delete OAuth2 client registration template by id (deleteClientRegistrationTemplate)" + SYSTEM_AUTHORITY_PARAGRAPH, + notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/{clientRegistrationTemplateId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteClientRegistrationTemplate(@PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException { + public void deleteClientRegistrationTemplate(@ApiParam(value = "String representation of client registration template id to delete", example = "139b1f81-2f5d-11ec-9dbe-9b627e1a88f4") + @PathVariable(CLIENT_REGISTRATION_TEMPLATE_ID) String strClientRegistrationTemplateId) throws ThingsboardException { checkParameter(CLIENT_REGISTRATION_TEMPLATE_ID, strClientRegistrationTemplateId); try { accessControlService.checkPermission(getCurrentUser(), Resource.OAUTH2_CONFIGURATION_TEMPLATE, Operation.DELETE); @@ -63,6 +79,8 @@ public class OAuth2ConfigTemplateController extends BaseController { } } + @ApiOperation(value = "Get the list of all OAuth2 client registration templates (getClientRegistrationTemplates)" + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + notes = OAUTH2_CLIENT_REGISTRATION_TEMPLATE_DEFINITION) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(method = RequestMethod.GET, produces = "application/json") @ResponseBody @@ -74,4 +92,5 @@ public class OAuth2ConfigTemplateController extends BaseController { throw handleException(e); } } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java index 7497e20122..3b323ca411 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/OAuth2Controller.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -41,6 +43,8 @@ import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -50,10 +54,20 @@ public class OAuth2Controller extends BaseController { @Autowired private OAuth2Configuration oAuth2Configuration; + + @ApiOperation(value = "Get OAuth2 clients (getOAuth2Clients)", notes = "Get the list of OAuth2 clients " + + "to log in with, available for such domain scheme (HTTP or HTTPS) (if x-forwarded-proto request header is present - " + + "the scheme is known from it) and domain name and port (port may be known from x-forwarded-port header)") @RequestMapping(value = "/noauth/oauth2Clients", method = RequestMethod.POST) @ResponseBody public List getOAuth2Clients(HttpServletRequest request, + @ApiParam(value = "Mobile application package name, to find OAuth2 clients " + + "where there is configured mobile application with such package name") @RequestParam(required = false) String pkgName, + @ApiParam(value = "Platform type to search OAuth2 clients for which " + + "the usage with this platform type is allowed in the settings. " + + "If platform type is not one of allowable values - it will just be ignored", + allowableValues = "WEB, ANDROID, IOS") @RequestParam(required = false) String platform) throws ThingsboardException { try { if (log.isDebugEnabled()) { @@ -76,6 +90,7 @@ public class OAuth2Controller extends BaseController { } } + @ApiOperation(value = "Get current OAuth2 settings (getCurrentOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/oauth2/config", method = RequestMethod.GET, produces = "application/json") @ResponseBody @@ -88,6 +103,7 @@ public class OAuth2Controller extends BaseController { } } + @ApiOperation(value = "Save OAuth2 settings (saveOAuth2Info)", notes = SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/oauth2/config", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) @@ -101,6 +117,10 @@ public class OAuth2Controller extends BaseController { } } + @ApiOperation(value = "Get OAuth2 log in processing URL (getLoginProcessingUrl)", notes = "Returns the URL enclosed in " + + "double quotes. After successful authentication with OAuth2 provider, it makes a redirect to this path so that the platform can do " + + "further log in processing. This URL may be configured as 'security.oauth2.loginProcessingUrl' property in yml configuration file, or " + + "as 'SECURITY_OAUTH2_LOGIN_PROCESSING_URL' env variable. By default it is '/login/oauth2/code/'" + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/oauth2/loginProcessingUrl", method = RequestMethod.GET) @ResponseBody @@ -112,4 +132,5 @@ public class OAuth2Controller extends BaseController { throw handleException(e); } } + } diff --git a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java index 4c0a55da70..593914bc4c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java +++ b/application/src/main/java/org/thingsboard/server/controller/OtaPackageController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.core.io.ByteArrayResource; @@ -47,6 +49,22 @@ import org.thingsboard.server.service.security.permission.Resource; import java.nio.ByteBuffer; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_CHECKSUM_ALGORITHM_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION; +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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @Slf4j @RestController @TbCoreComponent @@ -56,10 +74,12 @@ public class OtaPackageController extends BaseController { public static final String OTA_PACKAGE_ID = "otaPackageId"; public static final String CHECKSUM_ALGORITHM = "checksumAlgorithm"; + @ApiOperation(value = "Download OTA Package (downloadOtaPackage)", notes = "Download OTA Package based on the provided OTA Package Id." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority( 'TENANT_ADMIN')") @RequestMapping(value = "/otaPackage/{otaPackageId}/download", method = RequestMethod.GET) @ResponseBody - public ResponseEntity downloadOtaPackage(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { + public ResponseEntity downloadOtaPackage(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) + @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); try { OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); @@ -81,10 +101,15 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Get OTA Package Info (getOtaPackageInfoById)", + notes = "Fetch the OTA Package Info object based on the provided OTA Package Id. " + + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/otaPackage/info/{otaPackageId}", method = RequestMethod.GET) @ResponseBody - public OtaPackageInfo getOtaPackageInfoById(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { + public OtaPackageInfo getOtaPackageInfoById(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) + @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); try { OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); @@ -94,10 +119,15 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Get OTA Package (getOtaPackageById)", + notes = "Fetch the OTA Package object based on the provided OTA Package Id. " + + "The server checks that the OTA Package is owned by the same tenant. " + OTA_PACKAGE_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.GET) @ResponseBody - public OtaPackage getOtaPackageById(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { + public OtaPackage getOtaPackageById(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) + @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); try { OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); @@ -107,10 +137,19 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Create Or Update OTA Package Info (saveOtaPackageInfo)", + notes = "Create or update the OTA Package Info. When creating OTA Package Info, platform generates OTA Package id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created OTA Package id will be present in the response. " + + "Specify existing OTA Package id to update the OTA Package Info. " + + "Referencing non-existing OTA Package Id will cause 'Not Found' error. " + + "\n\nOTA Package combination of the title with the version is unique in the scope of tenant. " + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json", + consumes = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/otaPackage", method = RequestMethod.POST) @ResponseBody - public OtaPackageInfo saveOtaPackageInfo(@RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { + public OtaPackageInfo saveOtaPackageInfo(@ApiParam(value = "A JSON value representing the OTA Package.") + @RequestBody SaveOtaPackageInfoRequest otaPackageInfo) throws ThingsboardException { boolean created = otaPackageInfo.getId() == null; try { otaPackageInfo.setTenantId(getTenantId()); @@ -126,13 +165,20 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Save OTA Package data (saveOtaPackageData)", + notes = "Update the OTA Package. Adds the date to the existing OTA Package Info" + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.POST) @ResponseBody - public OtaPackageInfo saveOtaPackageData(@PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, - @RequestParam(required = false) String checksum, - @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, - @RequestBody MultipartFile file) throws ThingsboardException { + public OtaPackageInfo saveOtaPackageData(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) + @PathVariable(OTA_PACKAGE_ID) String strOtaPackageId, + @ApiParam(value = "OTA Package checksum. For example, '0xd87f7e0c'") + @RequestParam(required = false) String checksum, + @ApiParam(value = "OTA Package checksum algorithm.", allowableValues = OTA_PACKAGE_CHECKSUM_ALGORITHM_ALLOWABLE_VALUES) + @RequestParam(CHECKSUM_ALGORITHM) String checksumAlgorithmStr, + @ApiParam(value = "OTA Package data.") + @RequestBody MultipartFile file) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); checkParameter(CHECKSUM_ALGORITHM, checksumAlgorithmStr); try { @@ -171,14 +217,23 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", + notes = "Returns a page of OTA Package Info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/otaPackages", method = RequestMethod.GET) @ResponseBody - public PageData getOtaPackages(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getOtaPackages(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = OTA_PACKAGE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantId(getTenantId(), pageLink)); @@ -187,15 +242,26 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Get OTA Package Infos (getOtaPackages)", + notes = "Returns a page of OTA Package Info objects owned by tenant. " + + PAGE_DATA_PARAMETERS + OTA_PACKAGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET) @ResponseBody - public PageData getOtaPackages(@PathVariable("deviceProfileId") String strDeviceProfileId, + public PageData getOtaPackages(@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("deviceProfileId") String strDeviceProfileId, + @ApiParam(value = "OTA Package type.", allowableValues = "FIRMWARE, SOFTWARE") @PathVariable("type") String strType, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = OTA_PACKAGE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("deviceProfileId", strDeviceProfileId); checkParameter("type", strType); @@ -208,10 +274,15 @@ public class OtaPackageController extends BaseController { } } + @ApiOperation(value = "Delete OTA Package (deleteOtaPackage)", + notes = "Deletes the OTA Package. Referencing non-existing OTA Package Id will cause an error. " + + "Can't delete the OTA Package if it is referenced by existing devices or device profile." + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/otaPackage/{otaPackageId}", method = RequestMethod.DELETE) @ResponseBody - public void deleteOtaPackage(@PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { + public void deleteOtaPackage(@ApiParam(value = OTA_PACKAGE_ID_PARAM_DESCRIPTION) + @PathVariable("otaPackageId") String strOtaPackageId) throws ThingsboardException { checkParameter(OTA_PACKAGE_ID, strOtaPackageId); try { OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); 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 ad3b05d987..c1c57f2d1f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -15,7 +15,10 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -29,6 +32,10 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.Set; +import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -37,10 +44,13 @@ public class QueueController extends BaseController { private final QueueService queueService; + @ApiOperation(value = "Get queue names (getTenantQueuesByServiceType)", + notes = "Returns a set of unique queue names based on service type. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/tenant/queues", params = {"serviceType"}, method = RequestMethod.GET) - @ResponseBody - public Set getTenantQueuesByServiceType(@RequestParam String serviceType) throws ThingsboardException { + @RequestMapping(value = "/tenant/queues", params = {"serviceType"}, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) + @ResponseBody() + public Set getTenantQueuesByServiceType(@ApiParam(value = QUEUE_SERVICE_TYPE_DESCRIPTION, allowableValues = QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES) + @RequestParam String serviceType) throws ThingsboardException { checkParameter("serviceType", serviceType); try { return queueService.getQueuesByServiceType(ServiceType.valueOf(serviceType)); diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java index 9bece05110..a2c52d76a2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV1Controller.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -32,23 +34,36 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.UUID; +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + @RestController @TbCoreComponent @RequestMapping(TbUrlConstants.RPC_V1_URL_PREFIX) @Slf4j public class RpcV1Controller extends AbstractRpcController { + @ApiOperation(value = "Send one-way RPC request (handleOneWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException { + public DeferredResult handleOneWayDeviceRPCRequest( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable("deviceId") String deviceIdStr, + @ApiParam(value = "A JSON value representing the RPC request.") + @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } + @ApiOperation(value = "Send two-way RPC request (handleTwoWayDeviceRPCRequest)", notes = "Deprecated. See 'Rpc V 2 Controller' instead." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException { + public DeferredResult handleTwoWayDeviceRPCRequest( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable("deviceId") String deviceIdStr, + @ApiParam(value = "A JSON value representing the RPC request.") + @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.REQUEST_TIMEOUT, HttpStatus.CONFLICT); } 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 ae9081c39a..389a767cdf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -15,6 +15,11 @@ */ package org.thingsboard.server.controller; +import com.google.common.util.concurrent.FutureCallback; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -41,10 +46,28 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rpc.RemoveRpcActorMsg; import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.telemetry.exception.ToErrorResponseEntity; +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; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +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.RPC_ID; +import static org.thingsboard.server.controller.ControllerConstants.RPC_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RPC_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.RPC_STATUS_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.RPC_TEXT_SEARCH_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.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; @RestController @TbCoreComponent @@ -52,24 +75,88 @@ import static org.thingsboard.server.common.data.DataConstants.RPC_DELETED; @Slf4j public class RpcV2Controller extends AbstractRpcController { + private static final String RPC_REQUEST_DESCRIPTION = "Sends the one-way remote-procedure call (RPC) request to device. " + + "The RPC call is A JSON that contains the method name ('method'), parameters ('params') and multiple optional fields. " + + "See example below. We will review the properties of the RPC call one-by-one below. " + + "\n\n" + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"method\": \"setGpio\",\n" + + " \"params\": {\n" + + " \"pin\": 7,\n" + + " \"value\": 1\n" + + " },\n" + + " \"persistent\": false,\n" + + " \"timeout\": 5000\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n### Server-side RPC structure\n" + + "\n" + + "The body of server-side RPC request consists of multiple fields:\n" + + "\n" + + "* **method** - mandatory, name of the method to distinct the RPC calls.\n" + + " For example, \"getCurrentTime\" or \"getWeatherForecast\". The value of the parameter is a string.\n" + + "* **params** - mandatory, parameters used for processing of the request. The value is a JSON. Leave empty JSON \"{}\" if no parameters needed.\n" + + "* **timeout** - optional, value of the processing timeout in milliseconds. The default value is 10000 (10 seconds). The minimum value is 5000 (5 seconds).\n" + + "* **expirationTime** - optional, value of the epoch time (in milliseconds, UTC timezone). Overrides **timeout** if present.\n" + + "* **persistent** - optional, indicates persistent RPC. The default value is \"false\".\n" + + "* **retries** - optional, defines how many times persistent RPC will be re-sent in case of failures on the network and/or device side.\n" + + "* **additionalInfo** - optional, defines metadata for the persistent RPC that will be added to the persistent RPC events."; + + private static final String ONE_WAY_RPC_RESULT = "\n\n### RPC Result\n" + + "In case of persistent RPC, the result of this call is 'rpcId' UUID. In case of lightweight RPC, " + + "the result of this call is either 200 OK if the message was sent to device, or 504 Gateway Timeout if device is offline."; + + private static final String TWO_WAY_RPC_RESULT = "\n\n### RPC Result\n" + + "In case of persistent RPC, the result of this call is 'rpcId' UUID. In case of lightweight RPC, " + + "the result of this call is the response from device, or 504 Gateway Timeout if device is offline."; + + private static final String ONE_WAY_RPC_REQUEST_DESCRIPTION = "Sends the one-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + ONE_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + + private static final String TWO_WAY_RPC_REQUEST_DESCRIPTION = "Sends the two-way remote-procedure call (RPC) request to device. " + RPC_REQUEST_DESCRIPTION + TWO_WAY_RPC_RESULT + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; + + @ApiOperation(value = "Send one-way RPC request", notes = ONE_WAY_RPC_REQUEST_DESCRIPTION) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Persistent RPC request was saved to the database or lightweight RPC request was sent to the device."), + @ApiResponse(code = 400, message = "Invalid structure of the request."), + @ApiResponse(code = 401, message = "User is not authorized to send the RPC request. Most likely, User belongs to different Customer or Tenant."), + @ApiResponse(code = 504, message = "Timeout to process the RPC call. Most likely, device is offline."), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/oneway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleOneWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException { + public DeferredResult handleOneWayDeviceRPCRequest( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable("deviceId") String deviceIdStr, + @ApiParam(value = "A JSON value representing the RPC request.") + @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(true, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } + @ApiOperation(value = "Send two-way RPC request", notes = TWO_WAY_RPC_REQUEST_DESCRIPTION) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Persistent RPC request was saved to the database or lightweight RPC response received."), + @ApiResponse(code = 400, message = "Invalid structure of the request."), + @ApiResponse(code = 401, message = "User is not authorized to send the RPC request. Most likely, User belongs to different Customer or Tenant."), + @ApiResponse(code = 504, message = "Timeout to process the RPC call. Most likely, device is offline."), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/twoway/{deviceId}", method = RequestMethod.POST) @ResponseBody - public DeferredResult handleTwoWayDeviceRPCRequest(@PathVariable("deviceId") String deviceIdStr, @RequestBody String requestBody) throws ThingsboardException { + public DeferredResult handleTwoWayDeviceRPCRequest( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) + @PathVariable(DEVICE_ID) String deviceIdStr, + @ApiParam(value = "A JSON value representing the RPC request.") + @RequestBody String requestBody) throws ThingsboardException { return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody, HttpStatus.GATEWAY_TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } + @ApiOperation(value = "Get persistent RPC request", notes = "Get information about the status of the RPC call." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.GET) @ResponseBody - public Rpc getPersistedRpc(@PathVariable("rpcId") String strRpc) throws ThingsboardException { + public Rpc getPersistedRpc( + @ApiParam(value = RPC_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(RPC_ID) String strRpc) throws ThingsboardException { checkParameter("RpcId", strRpc); try { RpcId rpcId = new RpcId(UUID.fromString(strRpc)); @@ -79,31 +166,62 @@ public class RpcV2Controller extends AbstractRpcController { } } + @ApiOperation(value = "Get persistent RPC requests", notes = "Allows to query RPC calls for specific device using pagination." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/persistent/device/{deviceId}", method = RequestMethod.GET) @ResponseBody - public PageData getPersistedRpcByDevice(@PathVariable("deviceId") String strDeviceId, - @RequestParam int pageSize, - @RequestParam int page, - @RequestParam RpcStatus rpcStatus, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public DeferredResult getPersistedRpcByDevice( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(DEVICE_ID) String strDeviceId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = "Status of the RPC", required = true, allowableValues = RPC_STATUS_ALLOWABLE_VALUES) + @RequestParam RpcStatus rpcStatus, + @ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RPC_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("DeviceId", strDeviceId); try { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); DeviceId deviceId = new DeviceId(UUID.fromString(strDeviceId)); - return checkNotNull(rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink)); + final DeferredResult response = new DeferredResult<>(); + accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() { + @Override + public void onSuccess(@Nullable DeferredResult result) { + PageData rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK)); + } + + @Override + public void onFailure(Throwable e) { + ResponseEntity entity; + if (e instanceof ToErrorResponseEntity) { + entity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); + } else { + entity = new ResponseEntity(HttpStatus.UNAUTHORIZED); + } + response.setResult(entity); + } + })); + return response; } catch (Exception e) { throw handleException(e); } } + @ApiOperation(value = "Delete persistent RPC", notes = "Deletes the persistent RPC request." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/persistent/{rpcId}", method = RequestMethod.DELETE) @ResponseBody - public void deleteResource(@PathVariable("rpcId") String strRpc) throws ThingsboardException { + public void deleteResource( + @ApiParam(value = RPC_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(RPC_ID) String strRpc) throws ThingsboardException { checkParameter("RpcId", strRpc); try { RpcId rpcId = new RpcId(UUID.fromString(strRpc)); diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index a71fb70e0a..8996367c36 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -20,12 +20,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -79,6 +81,25 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +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.RULE_CHAIN_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.RULE_CHAIN_TEXT_SEARCH_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RULE_CHAIN_TYPES_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.RULE_CHAIN_TYPE_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.TENANT_AUTHORITY_PARAGRAPH; + @Slf4j @RestController @TbCoreComponent @@ -93,6 +114,25 @@ public class RuleChainController extends BaseController { private static final ObjectMapper objectMapper = new ObjectMapper(); public static final int TIMEOUT = 20; + private static final String RULE_CHAIN_DESCRIPTION = "The rule chain object is lightweight and contains general information about the rule chain. " + + "List of rule nodes and their connection is stored in a separate 'metadata' object."; + private static final String RULE_CHAIN_METADATA_DESCRIPTION = "The metadata object contains information about the rule nodes and their connections."; + private static final String TEST_JS_FUNCTION = "Execute the JavaScript function and return the result. The format of request: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"script\": \"Your JS Function as String\",\n" + + " \"scriptType\": \"One of: update, generate, filter, switch, json, string\",\n" + + " \"argNames\": [\"msg\", \"metadata\", \"type\"],\n" + + " \"msg\": \"{\\\"temperature\\\": 42}\", \n" + + " \"metadata\": {\n" + + " \"deviceName\": \"Device A\",\n" + + " \"deviceType\": \"Thermometer\"\n" + + " },\n" + + " \"msgType\": \"POST_TELEMETRY_REQUEST\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Expected result JSON contains \"output\" and \"error\"."; + @Autowired private InstallScripts installScripts; @@ -108,10 +148,14 @@ public class RuleChainController extends BaseController { @Value("${actors.rule.chain.debug_mode_rate_limits_per_tenant.enabled}") private boolean debugPerTenantEnabled; + @ApiOperation(value = "Get Rule Chain (getRuleChainById)", + notes = "Fetch the Rule Chain object based on the provided Rule Chain Id. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) @ResponseBody - public RuleChain getRuleChainById(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public RuleChain getRuleChainById( + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); try { RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); @@ -121,10 +165,14 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Get Rule Chain (getRuleChainById)", + notes = "Fetch the Rule Chain Metadata object based on the provided Rule Chain Id. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/metadata", method = RequestMethod.GET) @ResponseBody - public RuleChainMetaData getRuleChainMetaData(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public RuleChainMetaData getRuleChainMetaData( + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); try { RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); @@ -135,11 +183,18 @@ public class RuleChainController extends BaseController { } } - + @ApiOperation(value = "Create Or Update Rule Chain (saveRuleChain)", + notes = "Create or update the Rule Chain. When creating Rule Chain, platform generates Rule Chain Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created Rule Chain Id will be present in the response. " + + "Specify existing Rule Chain id to update the rule chain. " + + "Referencing non-existing rule chain Id will cause 'Not Found' error." + + "\n\n" + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain", method = RequestMethod.POST) @ResponseBody - public RuleChain saveRuleChain(@RequestBody RuleChain ruleChain) throws ThingsboardException { + public RuleChain saveRuleChain( + @ApiParam(value = "A JSON value representing the rule chain.") + @RequestBody RuleChain ruleChain) throws ThingsboardException { try { boolean created = ruleChain.getId() == null; ruleChain.setTenantId(getCurrentUser().getTenantId()); @@ -173,10 +228,15 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Create Default Rule Chain", + notes = "Create rule chain from template, based on the specified name in the request. " + + "Creates the rule chain based on the template that is used to create root rule chain. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST) @ResponseBody - public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException { + public RuleChain saveRuleChain( + @ApiParam(value = "A JSON value representing the request.") + @RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException { try { checkNotNull(request); checkParameter(request.getName(), "name"); @@ -196,10 +256,14 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Set Root Rule Chain (setRootRuleChain)", + notes = "Makes the rule chain to be root rule chain. Updates previous root rule chain as well. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST) @ResponseBody - public RuleChain setRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public RuleChain setRootRuleChain( + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); try { RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); @@ -235,10 +299,14 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Update Rule Chain Metadata", + notes = "Updates the rule chain metadata. " + RULE_CHAIN_METADATA_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/metadata", method = RequestMethod.POST) @ResponseBody - public RuleChainMetaData saveRuleChainMetaData(@RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException { + public RuleChainMetaData saveRuleChainMetaData( + @ApiParam(value = "A JSON value representing the rule chain metadata.") + @RequestBody RuleChainMetaData ruleChainMetaData) throws ThingsboardException { try { TenantId tenantId = getTenantId(); if (debugPerTenantEnabled) { @@ -276,15 +344,24 @@ public class RuleChainController extends BaseController { } } + + @ApiOperation(value = "Get Rule Chains (getRuleChains)", + notes = "Returns a page of Rule Chains owned by tenant. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getRuleChains( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = RULE_CHAIN_TYPE_DESCRIPTION, allowableValues = RULE_CHAIN_TYPES_ALLOWABLE_VALUES) @RequestParam(value = "type", required = false) String typeStr, + @ApiParam(value = RULE_CHAIN_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); @@ -299,10 +376,15 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Delete rule chain (deleteRuleChain)", + notes = "Deletes the rule chain. Referencing non-existing rule chain Id will cause an error. " + + "Referencing rule chain that is used in the device profiles will cause an error." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public void deleteRuleChain( + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) + @PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { checkParameter(RULE_CHAIN_ID, strRuleChainId); try { RuleChainId ruleChainId = new RuleChainId(toUUID(strRuleChainId)); @@ -345,10 +427,15 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Get latest input message (getLatestRuleNodeDebugInput)", + notes = "Gets the input message from the debug events for specified Rule Chain Id. " + + "Referencing non-existing rule chain Id will cause an error. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleNode/{ruleNodeId}/debugIn", method = RequestMethod.GET) @ResponseBody - public JsonNode getLatestRuleNodeDebugInput(@PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { + public JsonNode getLatestRuleNodeDebugInput( + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION) + @PathVariable(RULE_NODE_ID) String strRuleNodeId) throws ThingsboardException { checkParameter(RULE_NODE_ID, strRuleNodeId); try { RuleNodeId ruleNodeId = new RuleNodeId(toUUID(strRuleNodeId)); @@ -371,10 +458,15 @@ public class RuleChainController extends BaseController { } } + + @ApiOperation(value = "Test JavaScript function", + notes = TEST_JS_FUNCTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/testScript", method = RequestMethod.POST) @ResponseBody - public JsonNode testScript(@RequestBody JsonNode inputParams) throws ThingsboardException { + public JsonNode testScript( + @ApiParam(value = "Test JS request. See API call description above.") + @RequestBody JsonNode inputParams) throws ThingsboardException { try { String script = inputParams.get("script").asText(); String scriptType = inputParams.get("scriptType").asText(); @@ -434,10 +526,13 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Export Rule Chains", notes = "Exports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET) @ResponseBody - public RuleChainData exportRuleChains(@RequestParam("limit") int limit) throws ThingsboardException { + public RuleChainData exportRuleChains( + @ApiParam(value = "A limit of rule chains to export.", required = true) + @RequestParam("limit") int limit) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); PageLink pageLink = new PageLink(limit); @@ -447,18 +542,25 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Import Rule Chains", notes = "Imports all tenant rule chains as one JSON." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST) @ResponseBody - public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { + public List importRuleChains( + @ApiParam(value = "A JSON value representing the rule chains.") + @RequestBody RuleChainData ruleChainData, + @ApiParam(value = "Enables overwrite for existing rule chains with the same name.") + @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, RuleChainType.CORE, overwrite); - if (!CollectionUtils.isEmpty(importResults)) { - for (RuleChainImportResult importResult : importResults) { - tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent()); + List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite); + for (RuleChainImportResult importResult : importResults) { + if (importResult.getError() == null) { + tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(), + importResult.isUpdated() ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED); } } + return importResults; } catch (Exception e) { throw handleException(e); } @@ -494,6 +596,14 @@ public class RuleChainController extends BaseController { return msgData; } + @ApiOperation(value = "Assign rule chain to edge (assignRuleChainToEdge)", + notes = "Creates assignment of an existing rule chain to an instance of The Edge. " + + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive a copy of assignment rule chain " + + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once rule chain will be delivered to edge service, it's going to start processing messages locally. " + + "\n\nOnly rule chain with type 'EDGE' can be assigned to edge." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.POST) @ResponseBody @@ -527,6 +637,13 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Unassign rule chain from edge (unassignRuleChainFromEdge)", + notes = "Clears assignment of the rule chain to the edge. " + + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION + + "Second, remote edge service will receive an 'unassign' command to remove rule chain " + + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " + + "Third, once 'unassign' command will be delivered to edge service, it's going to remove rule chain locally." + TENANT_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/ruleChain/{ruleChainId}", method = RequestMethod.DELETE) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index 97020ca85d..3f083c3572 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; @@ -45,6 +47,22 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.Base64; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES; +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.RESOURCE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_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.TENANT_AUTHORITY_PARAGRAPH; + @Slf4j @RestController @TbCoreComponent @@ -53,10 +71,12 @@ public class TbResourceController extends BaseController { public static final String RESOURCE_ID = "resourceId"; + @ApiOperation(value = "Download Resource (downloadResource)", notes = "Download Resource based on the provided Resource Id." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/{resourceId}/download", method = RequestMethod.GET) @ResponseBody - public ResponseEntity downloadResource(@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + public ResponseEntity downloadResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); try { TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); @@ -74,10 +94,15 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get Resource Info (getResourceInfoById)", + notes = "Fetch the Resource Info object based on the provided Resource Id. " + + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/info/{resourceId}", method = RequestMethod.GET) @ResponseBody - public TbResourceInfo getResourceInfoById(@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + public TbResourceInfo getResourceInfoById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); try { TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); @@ -87,10 +112,15 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get Resource (getResourceById)", + notes = "Fetch the Resource object based on the provided Resource Id. " + + RESOURCE_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.GET) @ResponseBody - public TbResource getResourceById(@PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { + public TbResource getResourceById(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable(RESOURCE_ID) String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); try { TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); @@ -100,10 +130,19 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Create Or Update Resource (saveResource)", + notes = "Create or update the Resource. When creating the Resource, platform generates Resource id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created Resource id will be present in the response. " + + "Specify existing Resource id to update the Resource. " + + "Referencing non-existing Resource Id will cause 'Not Found' error. " + + "\n\nResource combination of the title with the key is unique in the scope of tenant. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json", + consumes = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource", method = RequestMethod.POST) @ResponseBody - public TbResource saveResource(@RequestBody TbResource resource) throws ThingsboardException { + public TbResource saveResource(@ApiParam(value = "A JSON value representing the Resource.") + @RequestBody TbResource resource) throws ThingsboardException { boolean created = resource.getId() == null; try { resource.setTenantId(getTenantId()); @@ -120,13 +159,22 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get Resource Infos (getResources)", + notes = "Returns a page of Resource Info objects owned by tenant or sysadmin. " + + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource", method = RequestMethod.GET) @ResponseBody - public PageData getResources(@RequestParam int pageSize, + public PageData getResources(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); @@ -140,13 +188,22 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjectsPage)", + notes = "Returns a page of LwM2M objects parsed from Resources with type 'LWM2M_MODEL' owned by tenant or sysadmin. " + + PAGE_DATA_PARAMETERS + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/resource/lwm2m/page", method = RequestMethod.GET) @ResponseBody - public List getLwm2mListObjectsPage(@RequestParam int pageSize, + public List getLwm2mListObjectsPage(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = new PageLink(pageSize, page, textSearch); @@ -156,11 +213,18 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get LwM2M Objects (getLwm2mListObjects)", + notes = "Returns a page of LwM2M objects parsed from Resources with type 'LWM2M_MODEL' owned by tenant or sysadmin. " + + "You can specify parameters to filter the results. " + LWM2M_OBJECT_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, + produces = "application/json") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/resource/lwm2m", method = RequestMethod.GET) @ResponseBody - public List getLwm2mListObjects(@RequestParam String sortOrder, + public List getLwm2mListObjects(@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES, required = true) + @RequestParam String sortOrder, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES, required = true) @RequestParam String sortProperty, + @ApiParam(value = "LwM2M Object ids.", required = true) @RequestParam(required = false) String[] objectIds) throws ThingsboardException { try { return checkNotNull(resourceService.findLwM2mObject(getTenantId(), sortOrder, sortProperty, objectIds)); @@ -169,10 +233,13 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Delete Resource (deleteResource)", + notes = "Deletes the Resource. Referencing non-existing Resource Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/resource/{resourceId}", method = RequestMethod.DELETE) @ResponseBody - public void deleteResource(@PathVariable("resourceId") String strResourceId) throws ThingsboardException { + public void deleteResource(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION) + @PathVariable("resourceId") String strResourceId) throws ThingsboardException { checkParameter(RESOURCE_ID, strResourceId); try { TbResourceId resourceId = new TbResourceId(toUUID(strResourceId)); 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 d311cdbfed..4919c0019b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -26,10 +24,15 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; import com.google.gson.JsonParser; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.util.StringUtils; @@ -41,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.data.DataConstants; @@ -48,7 +52,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; -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.EntityIdFactory; @@ -86,7 +89,6 @@ import org.thingsboard.server.service.telemetry.exception.UncheckedApiException; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -99,6 +101,43 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_JSON_REQUEST_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_KEYS_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_ALLOWED_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTE_DATA_EXAMPLE; +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.ENTITY_ATTRIBUTE_SCOPES; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.INVALID_STRUCTURE_OF_THE_REQUEST; +import static org.thingsboard.server.controller.ControllerConstants.LATEST_TS_NON_STRICT_DATA_EXAMPLE; +import static org.thingsboard.server.controller.ControllerConstants.LATEST_TS_STRICT_DATA_EXAMPLE; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ATTIRIBUTES_STATUS_OK; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ATTRIBUTES_REQUEST_PAYLOAD; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_ATTRIBUTES_STATUS_OK; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_TIMESERIES_STATUS_OK; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED; +import static org.thingsboard.server.controller.ControllerConstants.SAVE_TIMESERIES_REQUEST_PAYLOAD; +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.STRICT_DATA_TYPES_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_JSON_REQUEST_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_KEYS_BASE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_KEYS_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_SCOPE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TS_STRICT_DATA_EXAMPLE; + + /** * Created by ashvayka on 22.03.18. */ @@ -119,8 +158,6 @@ public class TelemetryController extends BaseController { private ExecutorService executor; - private static final ObjectMapper mapper = new ObjectMapper(); - @PostConstruct public void initExecutor() { executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("telemetry-controller")); @@ -133,83 +170,159 @@ public class TelemetryController extends BaseController { } } + @ApiOperation(value = "Get all attribute keys (getAttributeKeys)", + notes = "Returns a set of unique attribute key names for the selected entity. " + + "The response will include merged key names set for all attribute scopes:" + + "\n\n * SERVER_SCOPE - supported for all entity types;" + + "\n * CLIENT_SCOPE - supported for devices;" + + "\n * SHARED_SCOPE - supported for devices. " + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET) @ResponseBody public DeferredResult getAttributeKeys( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException { + @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_ATTRIBUTES, entityType, entityIdStr, this::getAttributeKeysCallback); } + @ApiOperation(value = "Get all attribute keys by scope (getAttributeKeysByScope)", + notes = "Returns a set of unique attribute key names for the selected entity and attributes scope: " + + "\n\n * SERVER_SCOPE - supported for all entity types;" + + "\n * CLIENT_SCOPE - supported for devices;" + + "\n * SHARED_SCOPE - supported for devices. " + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET) @ResponseBody public DeferredResult getAttributeKeysByScope( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr - , @PathVariable("scope") String scope) throws ThingsboardException { + @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_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)); } + @ApiOperation(value = "Get attributes (getAttributes)", + notes = "Returns all attributes that belong to specified entity. Use optional 'keys' parameter to return specific attributes." + + "\n Example of the result: \n\n" + + MARKDOWN_CODE_BLOCK_START + + ATTRIBUTE_DATA_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET) @ResponseBody public DeferredResult getAttributes( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @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(); return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); } + + @ApiOperation(value = "Get attributes by scope (getAttributesByScope)", + notes = "Returns all attributes of a specified scope that belong to specified entity." + + ENTITY_ATTRIBUTE_SCOPES + + "Use optional 'keys' parameter to return specific attributes." + + "\n Example of the result: \n\n" + + MARKDOWN_CODE_BLOCK_START + + ATTRIBUTE_DATA_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET) @ResponseBody public DeferredResult getAttributesByScope( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @PathVariable("scope") String scope, - @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + @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_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, + @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)); } + @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)", + notes = "Returns a set of unique time-series key names for the selected entity. " + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET) @ResponseBody public DeferredResult getTimeseriesKeys( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException { + @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())); } + @ApiOperation(value = "Get latest time-series value (getLatestTimeseries)", + notes = "Returns all time-series that belong to specified entity. Use optional 'keys' parameter to return specific time-series." + + " The result is a JSON object. The format of the values depends on the 'useStrictDataTypes' parameter." + + " By default, all time-series values are converted to strings: \n\n" + + MARKDOWN_CODE_BLOCK_START + + LATEST_TS_NON_STRICT_DATA_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + "\n\n However, it is possible to request the values without conversion ('useStrictDataTypes'=true): \n\n" + + MARKDOWN_CODE_BLOCK_START + + LATEST_TS_STRICT_DATA_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET) @ResponseBody public DeferredResult getLatestTimeseries( - @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @RequestParam(name = "keys", required = false) String keysStr, + @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 = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr, + @ApiParam(value = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { SecurityUser user = getCurrentUser(); - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); } - + @ApiOperation(value = "Get time-series data (getTimeseries)", + notes = "Returns a range of time-series values for specified entity. " + + "Returns not aggregated data by default. " + + "Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " + + "The aggregation is generally more efficient then fetching all records. \n\n" + + MARKDOWN_CODE_BLOCK_START + + TS_STRICT_DATA_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"}) @ResponseBody public DeferredResult getTimeseries( - @PathVariable("entityType") String entityType, - @PathVariable("entityId") String entityIdStr, - @RequestParam(name = "keys") String keys, + @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 = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys, + @ApiParam(value = "A long value representing the start timestamp of the time range in milliseconds, UTC.") @RequestParam(name = "startTs") Long startTs, + @ApiParam(value = "A long value representing the end timestamp of the time range in milliseconds, UTC.") @RequestParam(name = "endTs") Long endTs, + @ApiParam(value = "A long value representing the aggregation interval range in milliseconds.") @RequestParam(name = "interval", defaultValue = "0") Long interval, + @ApiParam(value = "An integer value that represents a max number of timeseries data points to fetch." + + " This parameter is used only in the case if 'agg' parameter is set to 'NONE'.", defaultValue = "100") @RequestParam(name = "limit", defaultValue = "100") Integer limit, + @ApiParam(value = "A string value representing the aggregation function. " + + "If the interval is not specified, 'agg' parameter will use 'NONE' value.", + allowableValues = "MIN, MAX, AVG, SUM, COUNT, NONE") @RequestParam(name = "agg", defaultValue = "NONE") String aggStr, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(name = "orderBy", defaultValue = "DESC") String orderBy, + @ApiParam(value = STRICT_DATA_TYPES_DESCRIPTION) @RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException { return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, (result, tenantId, entityId) -> { @@ -222,64 +335,160 @@ public class TelemetryController extends BaseController { }); } + @ApiOperation(value = "Save device attributes (saveDeviceAttributes)", + notes = "Creates or updates the device attributes based on device id and specified attribute scope. " + + SAVE_ATTRIBUTES_REQUEST_PAYLOAD + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = SAVE_ATTIRIBUTES_STATUS_OK + + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED', " + + "and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'."), + @ApiResponse(code = 400, message = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), + @ApiResponse(code = 401, message = "User is not authorized to save device attributes for selected device. Most likely, User belongs to different Customer or Tenant."), + @ApiResponse(code = 500, message = "The exception was thrown during processing the request. " + + "Platform creates an audit log event about device attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace."), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) @ResponseBody - public DeferredResult saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope, - @RequestBody JsonNode request) throws ThingsboardException { + public DeferredResult saveDeviceAttributes( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr, + @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } + @ApiOperation(value = "Save entity attributes (saveEntityAttributesV1)", + notes = "Creates or updates the entity attributes based on Entity Id and the specified attribute scope. " + + ENTITY_ATTRIBUTE_SCOPES + + SAVE_ATTRIBUTES_REQUEST_PAYLOAD + + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = SAVE_ATTIRIBUTES_STATUS_OK + SAVE_ENTITY_ATTRIBUTES_STATUS_OK), + @ApiResponse(code = 400, message = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), + @ApiResponse(code = 401, message = SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED), + @ApiResponse(code = 500, message = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) @ResponseBody - public DeferredResult saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @PathVariable("scope") String scope, - @RequestBody JsonNode request) throws ThingsboardException { + public DeferredResult saveEntityAttributesV1( + @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_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } + @ApiOperation(value = "Save entity attributes (saveEntityAttributesV2)", + notes = "Creates or updates the entity attributes based on Entity Id and the specified attribute scope. " + + ENTITY_ATTRIBUTE_SCOPES + + SAVE_ATTRIBUTES_REQUEST_PAYLOAD + + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = SAVE_ATTIRIBUTES_STATUS_OK + SAVE_ENTITY_ATTRIBUTES_STATUS_OK), + @ApiResponse(code = 400, message = SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST), + @ApiResponse(code = 401, message = SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED), + @ApiResponse(code = 500, message = SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST) @ResponseBody - public DeferredResult saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @PathVariable("scope") String scope, - @RequestBody JsonNode request) throws ThingsboardException { + public DeferredResult saveEntityAttributesV2( + @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_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveAttributes(getTenantId(), entityId, scope, request); } + + @ApiOperation(value = "Save or update time-series data (saveEntityTelemetry)", + notes = "Creates or updates the entity time-series data based on the Entity Id and request payload." + + SAVE_TIMESERIES_REQUEST_PAYLOAD + + "\n\n The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. " + + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = SAVE_ENTITY_TIMESERIES_STATUS_OK), + @ApiResponse(code = 400, message = INVALID_STRUCTURE_OF_THE_REQUEST), + @ApiResponse(code = 401, message = SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED), + @ApiResponse(code = 500, message = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST) @ResponseBody - public DeferredResult saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @PathVariable("scope") String scope, - @RequestBody String requestBody) throws ThingsboardException { + public DeferredResult saveEntityTelemetry( + @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 = TELEMETRY_SCOPE_DESCRIPTION, required = true, allowableValues = "ANY") @PathVariable("scope") String scope, + @ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, 0L); } + @ApiOperation(value = "Save or update time-series data with TTL (saveEntityTelemetryWithTTL)", + notes = "Creates or updates the entity time-series data based on the Entity Id and request payload." + + SAVE_TIMESERIES_REQUEST_PAYLOAD + + "\n\n The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. " + + "\n\nThe ttl parameter takes affect only in case of Cassandra DB." + + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = SAVE_ENTITY_TIMESERIES_STATUS_OK), + @ApiResponse(code = 400, message = INVALID_STRUCTURE_OF_THE_REQUEST), + @ApiResponse(code = 401, message = SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED), + @ApiResponse(code = 500, message = SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST) @ResponseBody - public DeferredResult saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @PathVariable("scope") String scope, @PathVariable("ttl") Long ttl, - @RequestBody String requestBody) throws ThingsboardException { + public DeferredResult saveEntityTelemetryWithTTL( + @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 = TELEMETRY_SCOPE_DESCRIPTION, required = true, allowableValues = "ANY") @PathVariable("scope") String scope, + @ApiParam(value = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl, + @ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return saveTelemetry(getTenantId(), entityId, requestBody, ttl); } + @ApiOperation(value = "Delete entity time-series data (deleteEntityTimeseries)", + 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. " + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Timeseries for the selected keys in the request was removed. " + + "Platform creates an audit log event about entity timeseries removal with action type 'TIMESERIES_DELETED'."), + @ApiResponse(code = 400, message = "Platform returns a bad request in case if keys list is empty or start and end timestamp values is empty when deleteAllDataForKeys is set to false."), + @ApiResponse(code = 401, message = "User is not authorized to delete entity timeseries for selected entity. Most likely, User belongs to different Customer or Tenant."), + @ApiResponse(code = 500, message = "The exception was thrown during processing the request. " + + "Platform creates an audit log event about entity timeseries removal with action type 'TIMESERIES_DELETED' that includes an error stacktrace."), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE) @ResponseBody - public DeferredResult deleteEntityTimeseries(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @RequestParam(name = "keys") String keysStr, - @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys, - @RequestParam(name = "startTs", required = false) Long startTs, - @RequestParam(name = "endTs", required = false) Long endTs, - @RequestParam(name = "rewriteLatestIfDeleted", defaultValue = "false") boolean rewriteLatestIfDeleted) throws ThingsboardException { + public DeferredResult deleteEntityTimeseries( + @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 = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr, + @ApiParam(value = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.") + @RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys, + @ApiParam(value = "A long value representing the start timestamp of removal time range in milliseconds.") + @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 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); } @@ -311,9 +520,8 @@ public class TelemetryController extends BaseController { for (String key : keys) { deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted)); } - ListenableFuture> future = tsService.remove(user.getTenantId(), entityId, deleteTsKvQueries); - Futures.addCallback(future, new FutureCallback>() { + Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(@Nullable List tmp) { logTimeseriesDeleted(user, entityId, keys, deleteFromTs, deleteToTs, null); @@ -329,22 +537,49 @@ public class TelemetryController extends BaseController { }); } + @ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)", + notes = "Delete device attributes using provided Device Id, scope and a list of keys. " + + "Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Device attributes was removed for the selected keys in the request. " + + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED'."), + @ApiResponse(code = 400, message = "Platform returns a bad request in case if keys or scope are not specified."), + @ApiResponse(code = 401, message = "User is not authorized to delete device attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), + @ApiResponse(code = 500, message = "The exception was thrown during processing the request. " + + "Platform creates an audit log event about device attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) @ResponseBody - public DeferredResult deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr, - @PathVariable("scope") String scope, - @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + public DeferredResult deleteDeviceAttributes( + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr, + @ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); return deleteAttributes(entityId, scope, keysStr); } + @ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)", + notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " + + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Entity attributes was removed for the selected keys in the request. " + + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED'."), + @ApiResponse(code = 400, message = "Platform returns a bad request in case if keys or scope are not specified."), + @ApiResponse(code = 401, message = "User is not authorized to delete entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant."), + @ApiResponse(code = 500, message = "The exception was thrown during processing the request. " + + "Platform creates an audit log event about entity attributes removal with action type 'ATTRIBUTES_DELETED' that includes an error stacktrace."), + }) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) @ResponseBody - public DeferredResult deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, - @PathVariable("scope") String scope, - @RequestParam(name = "keys") String keysStr) throws ThingsboardException { + public DeferredResult deleteEntityAttributes( + @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_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope, + @ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException { EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); return deleteAttributes(entityId, scope, keysStr); } @@ -517,7 +752,7 @@ public class TelemetryController extends BaseController { } private FutureCallback> getTsKeysToResponseCallback(final DeferredResult response) { - return new FutureCallback>() { + return new FutureCallback<>() { @Override public void onSuccess(List values) { List keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList()); @@ -552,7 +787,7 @@ public class TelemetryController extends BaseController { private FutureCallback> getAttributeValuesToResponseCallback(final DeferredResult response, final SecurityUser user, final String scope, final EntityId entityId, final List keyList) { - return new FutureCallback>() { + return new FutureCallback<>() { @Override public void onSuccess(List attributes) { List values = attributes.stream().map(attribute -> @@ -572,7 +807,7 @@ public class TelemetryController extends BaseController { } private FutureCallback> getTsKvListCallback(final DeferredResult response, Boolean useStrictDataTypes) { - return new FutureCallback>() { + return new FutureCallback<>() { @Override public void onSuccess(List data) { Map> result = new LinkedHashMap<>(); @@ -692,16 +927,16 @@ public class TelemetryController extends BaseController { private String toJsonStr(JsonNode value) { try { - return mapper.writeValueAsString(value); - } catch (JsonProcessingException e) { + return JacksonUtil.toString(value); + } catch (IllegalArgumentException e) { throw new JsonParseException("Can't parse jsonValue: " + value, e); } } private JsonNode toJsonNode(String value) { try { - return mapper.readTree(value); - } catch (IOException e) { + return JacksonUtil.toJsonNode(value); + } catch (IllegalArgumentException e) { throw new JsonParseException("Can't parse jsonValue: " + value, e); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java index f8ac4acf49..edde16d6e5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java @@ -16,6 +16,8 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -41,27 +43,47 @@ import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; +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_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_TEXT_SEARCH_DESCRIPTION; + @RestController @TbCoreComponent @RequestMapping("/api") @Slf4j public class TenantController extends BaseController { + private static final String TENANT_INFO_DESCRIPTION = "The Tenant Info object extends regular Tenant object and includes Tenant Profile name. "; @Autowired private InstallScripts installScripts; @Autowired private TenantService tenantService; + @ApiOperation(value = "Get Tenant (getTenantById)", + notes = "Fetch the Tenant object based on the provided Tenant Id. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.GET) @ResponseBody - public Tenant getTenantById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException { - checkParameter("tenantId", strTenantId); + public Tenant getTenantById( + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) + @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { + checkParameter(TENANT_ID, strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); Tenant tenant = checkTenantId(tenantId, Operation.READ); - if(!tenant.getAdditionalInfo().isNull()) { + if (!tenant.getAdditionalInfo().isNull()) { processDashboardIdFromAdditionalInfo((ObjectNode) tenant.getAdditionalInfo(), HOME_DASHBOARD); } return tenant; @@ -70,11 +92,16 @@ public class TenantController extends BaseController { } } + @ApiOperation(value = "Get Tenant Info (getTenantInfoById)", + notes = "Fetch the Tenant Info object based on the provided Tenant Id. " + + TENANT_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET) @ResponseBody - public TenantInfo getTenantInfoById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException { - checkParameter("tenantId", strTenantId); + public TenantInfo getTenantInfoById( + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) + @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { + checkParameter(TENANT_ID, strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); return checkTenantInfoId(tenantId, Operation.READ); @@ -83,10 +110,19 @@ public class TenantController extends BaseController { } } + @ApiOperation(value = "Create Or update Tenant (saveTenant)", + notes = "Create or update the Tenant. When creating tenant, platform generates Tenant Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "Default Rule Chain and Device profile are also generated for the new tenants automatically. " + + "The newly created Tenant Id will be present in the response. " + + "Specify existing Tenant Id id to update the Tenant. " + + "Referencing non-existing Tenant Id will cause 'Not Found' error." + + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant", method = RequestMethod.POST) @ResponseBody - public Tenant saveTenant(@RequestBody Tenant tenant) throws ThingsboardException { + public Tenant saveTenant( + @ApiParam(value = "A JSON value representing the tenant.") + @RequestBody Tenant tenant) throws ThingsboardException { try { boolean newTenant = tenant.getId() == null; @@ -107,11 +143,15 @@ public class TenantController extends BaseController { } } + @ApiOperation(value = "Delete Tenant (deleteTenant)", + notes = "Deletes the tenant, it's customers, rule chains, devices and all other related entities. Referencing non-existing tenant Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteTenant(@PathVariable("tenantId") String strTenantId) throws ThingsboardException { - checkParameter("tenantId", strTenantId); + public void deleteTenant( + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION) + @PathVariable(TENANT_ID) String strTenantId) throws ThingsboardException { + checkParameter(TENANT_ID, strTenantId); try { TenantId tenantId = new TenantId(toUUID(strTenantId)); Tenant tenant = checkTenantId(tenantId, Operation.DELETE); @@ -124,14 +164,21 @@ public class TenantController extends BaseController { } } + @ApiOperation(value = "Get Tenants (getTenants)", notes = "Returns a page of tenants registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenants", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getTenants(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getTenants( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = TENANT_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(tenantService.findTenants(pageLink)); @@ -140,14 +187,23 @@ public class TenantController extends BaseController { } } + @ApiOperation(value = "Get Tenants Info (getTenants)", notes = "Returns a page of tenant info objects registered in the platform. " + + TENANT_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getTenantInfos(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getTenantInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = TENANT_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder + ) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(tenantService.findTenantInfos(pageLink)); diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java index e201c8f815..ab52e8ebd4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -38,16 +40,36 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END; +import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START; +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_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION; + @RestController @TbCoreComponent @RequestMapping("/api") @Slf4j public class TenantProfileController extends BaseController { + private static final String TENANT_PROFILE_INFO_DESCRIPTION = "Tenant Profile Info is a lightweight object that contains only id and name of the profile. "; + + @ApiOperation(value = "Get Tenant Profile (getTenantProfileById)", + notes = "Fetch the Tenant Profile object based on the provided Tenant Profile Id. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET) @ResponseBody - public TenantProfile getTenantProfileById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + public TenantProfile getTenantProfileById( + @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); try { TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); @@ -57,10 +79,14 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Get Tenant Profile Info (getTenantProfileInfoById)", + notes = "Fetch the Tenant Profile Info object based on the provided Tenant Profile Id. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET) @ResponseBody - public EntityInfo getTenantProfileInfoById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + public EntityInfo getTenantProfileInfoById( + @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); try { TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); @@ -70,6 +96,8 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Get default Tenant Profile Info (getDefaultTenantProfileInfo)", + notes = "Fetch the default Tenant Profile Info object based. " + TENANT_PROFILE_INFO_DESCRIPTION + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET) @ResponseBody @@ -81,10 +109,65 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Create Or update Tenant Profile (saveTenantProfile)", + notes = "Create or update the Tenant Profile. When creating tenant profile, platform generates Tenant Profile Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created Tenant Profile Id will be present in the response. " + + "Specify existing Tenant Profile Id id to update the Tenant Profile. " + + "Referencing non-existing Tenant Profile Id will cause 'Not Found' error. " + + "\n\nUpdate of the tenant profile configuration will cause immediate recalculation of API limits for all affected Tenants. " + + "\n\nThe **'profileData'** object is the part of Tenant Profile that defines API limits and Rate limits. " + + "\n\nYou have an ability to define maximum number of devices ('maxDevice'), assets ('maxAssets') and other entities. " + + "You may also define maximum number of messages to be processed per month ('maxTransportMessages', 'maxREExecutions', etc). " + + "The '*RateLimit' defines the rate limits using simple syntax. For example, '1000:1,20000:60' means up to 1000 events per second but no more than 20000 event per minute. " + + "Let's review the example of tenant profile data below: " + + "\n\n" + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"name\": \"Default\",\n" + + " \"description\": \"Default tenant profile\",\n" + + " \"isolatedTbCore\": false,\n" + + " \"isolatedTbRuleEngine\": false,\n" + + " \"profileData\": {\n" + + " \"configuration\": {\n" + + " \"type\": \"DEFAULT\",\n" + + " \"maxDevices\": 0,\n" + + " \"maxAssets\": 0,\n" + + " \"maxCustomers\": 0,\n" + + " \"maxUsers\": 0,\n" + + " \"maxDashboards\": 0,\n" + + " \"maxRuleChains\": 0,\n" + + " \"maxResourcesInBytes\": 0,\n" + + " \"maxOtaPackagesInBytes\": 0,\n" + + " \"transportTenantMsgRateLimit\": \"1000:1,20000:60\",\n" + + " \"transportTenantTelemetryMsgRateLimit\": \"1000:1,20000:60\",\n" + + " \"transportTenantTelemetryDataPointsRateLimit\": \"1000:1,20000:60\",\n" + + " \"transportDeviceMsgRateLimit\": \"20:1,600:60\",\n" + + " \"transportDeviceTelemetryMsgRateLimit\": \"20:1,600:60\",\n" + + " \"transportDeviceTelemetryDataPointsRateLimit\": \"20:1,600:60\",\n" + + " \"maxTransportMessages\": 10000000,\n" + + " \"maxTransportDataPoints\": 10000000,\n" + + " \"maxREExecutions\": 4000000,\n" + + " \"maxJSExecutions\": 5000000,\n" + + " \"maxDPStorageDays\": 0,\n" + + " \"maxRuleNodeExecutionsPerMessage\": 50,\n" + + " \"maxEmails\": 0,\n" + + " \"maxSms\": 0,\n" + + " \"maxCreatedAlarms\": 1000,\n" + + " \"defaultStorageTtlDays\": 0,\n" + + " \"alarmsTtlDays\": 0,\n" + + " \"rpcTtlDays\": 0,\n" + + " \"warnThreshold\": 0\n" + + " }\n" + + " },\n" + + " \"default\": true\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST) @ResponseBody - public TenantProfile saveTenantProfile(@RequestBody TenantProfile tenantProfile) throws ThingsboardException { + public TenantProfile saveTenantProfile( + @ApiParam(value = "A JSON value representing the tenant profile.") + @RequestBody TenantProfile tenantProfile) throws ThingsboardException { try { boolean newTenantProfile = tenantProfile.getId() == null; if (newTenantProfile) { @@ -105,10 +188,14 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Delete Tenant Profile (deleteTenantProfile)", + notes = "Deletes the tenant profile. Referencing non-existing tenant profile Id will cause an error. Referencing profile that is used by the tenants will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + public void deleteTenantProfile( + @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); try { TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); @@ -120,10 +207,14 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Make tenant profile default (setDefaultTenantProfile)", + notes = "Makes specified tenant profile to be default. Referencing non-existing tenant profile Id will cause an error. " + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST) @ResponseBody - public TenantProfile setDefaultTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { + public TenantProfile setDefaultTenantProfile( + @ApiParam(value = TENANT_PROFILE_ID_PARAM_DESCRIPTION) + @PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException { checkParameter("tenantProfileId", strTenantProfileId); try { TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId)); @@ -135,14 +226,21 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Get Tenant Profiles (getTenantProfiles)", notes = "Returns a page of tenant profiles registered in the platform. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getTenantProfiles(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getTenantProfiles( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink)); @@ -151,14 +249,22 @@ public class TenantProfileController extends BaseController { } } + @ApiOperation(value = "Get Tenant Profiles Info (getTenantProfileInfos)", notes = "Returns a page of tenant profile info objects registered in the platform. " + + TENANT_PROFILE_INFO_DESCRIPTION + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody - public PageData getTenantProfileInfos(@RequestParam int pageSize, - @RequestParam int page, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String sortProperty, - @RequestParam(required = false) String sortOrder) throws ThingsboardException { + public PageData getTenantProfileInfos( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) + @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) + @RequestParam int page, + @ApiParam(value = TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION) + @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink)); diff --git a/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java b/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java new file mode 100644 index 0000000000..e28e378119 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2021 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 io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.access.prepost.PreAuthorize; +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.exception.ThingsboardException; +import org.thingsboard.server.queue.util.TbCoreComponent; + +@RestController +@TbCoreComponent +@RequestMapping("/api") +public class UiSettingsController extends BaseController { + + @Value("${ui.help.base-url}") + private String helpBaseUrl; + + @ApiOperation(value = "Get UI help base url (getHelpBaseUrl)", + notes = "Get UI help base url used to fetch help assets. " + + "The actual value of the base url is configurable in the system configuration file.") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/uiSettings/helpBaseUrl", method = RequestMethod.GET) + @ResponseBody + public String getHelpBaseUrl() throws ThingsboardException { + return helpBaseUrl; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index 9daacc3027..d768a42e16 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.controller; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -32,7 +32,6 @@ 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.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; @@ -52,6 +51,7 @@ import org.thingsboard.server.common.data.security.event.UserAuthDataChangedEven import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository; +import org.thingsboard.server.service.security.model.JwtTokenPair; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; @@ -62,6 +62,26 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD; +import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD; +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_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.USER_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION; + @RequiredArgsConstructor @RestController @TbCoreComponent @@ -82,20 +102,27 @@ public class UserController extends BaseController { private final SystemSecurityService systemSecurityService; private final ApplicationEventPublisher eventPublisher; + @ApiOperation(value = "Get User (getUserById)", + notes = "Fetch the User object based on the provided User Id. " + + "If the user has the authority of 'SYS_ADMIN', the server does not perform additional checks. " + + "If the user has the authority of 'TENANT_ADMIN', the server checks that the requested user is owned by the same tenant. " + + "If the user has the authority of 'CUSTOMER_USER', the server checks that the requested user is owned by the same customer.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) @ResponseBody - public User getUserById(@PathVariable(USER_ID) String strUserId) throws ThingsboardException { + public User getUserById( + @ApiParam(value = USER_ID_PARAM_DESCRIPTION) + @PathVariable(USER_ID) String strUserId) throws ThingsboardException { checkParameter(USER_ID, strUserId); try { UserId userId = new UserId(toUUID(strUserId)); User user = checkUserId(userId, Operation.READ); - if(user.getAdditionalInfo().isObject()) { + if (user.getAdditionalInfo().isObject()) { ObjectNode additionalInfo = (ObjectNode) user.getAdditionalInfo(); processDashboardIdFromAdditionalInfo(additionalInfo, DEFAULT_DASHBOARD); processDashboardIdFromAdditionalInfo(additionalInfo, HOME_DASHBOARD); UserCredentials userCredentials = userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()); - if(userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { + if (userCredentials.isEnabled() && !additionalInfo.has("userCredentialsEnabled")) { additionalInfo.put("userCredentialsEnabled", true); } } @@ -105,6 +132,10 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Check Token Access Enabled (isUserTokenAccessEnabled)", + notes = "Checks that the system is configured to allow administrators to impersonate themself as other users. " + + "If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to login as any tenant administrator. " + + "If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to login as any customer user. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/tokenAccessEnabled", method = RequestMethod.GET) @ResponseBody @@ -112,10 +143,16 @@ public class UserController extends BaseController { return userTokenAccessEnabled; } + @ApiOperation(value = "Get User Token (getUserToken)", + notes = "Returns the token of the User based on the provided User Id. " + + "If the user who performs the request has the authority of 'SYS_ADMIN', it is possible to get the token of any tenant administrator. " + + "If the user who performs the request has the authority of 'TENANT_ADMIN', it is possible to get the token of any customer user that belongs to the same tenant. ") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/{userId}/token", method = RequestMethod.GET) @ResponseBody - public JsonNode getUserToken(@PathVariable(USER_ID) String strUserId) throws ThingsboardException { + public JwtTokenPair getUserToken( + @ApiParam(value = USER_ID_PARAM_DESCRIPTION) + @PathVariable(USER_ID) String strUserId) throws ThingsboardException { checkParameter(USER_ID, strUserId); try { if (!userTokenAccessEnabled) { @@ -130,22 +167,26 @@ public class UserController extends BaseController { SecurityUser securityUser = new SecurityUser(user, credentials.isEnabled(), principal); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser); - ObjectMapper objectMapper = new ObjectMapper(); - ObjectNode tokenObject = objectMapper.createObjectNode(); - tokenObject.put("token", accessToken.getToken()); - tokenObject.put("refreshToken", refreshToken.getToken()); - return tokenObject; + return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken()); } catch (Exception e) { throw handleException(e); } } + @ApiOperation(value = "Save Or update User (saveUser)", + notes = "Create or update the User. When creating user, platform generates User Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created User Id will be present in the response. " + + "Specify existing User Id to update the device. " + + "Referencing non-existing User Id will cause 'Not Found' error." + + "\n\nDevice email is unique for entire platform setup.") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/user", method = RequestMethod.POST) @ResponseBody - public User saveUser(@RequestBody User user, - @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, - HttpServletRequest request) throws ThingsboardException { + public User saveUser( + @ApiParam(value = "A JSON value representing the User.", required = true) + @RequestBody User user, + @ApiParam(value = "Send activation email (or use activation link)", defaultValue = "true") + @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail, HttpServletRequest request) throws ThingsboardException { try { if (Authority.TENANT_ADMIN.equals(getCurrentUser().getAuthority())) { @@ -188,10 +229,13 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Send or re-send the activation email", + notes = "Force send the activation email to the user. Useful to resend the email if user has accidentally deleted it. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/sendActivationMail", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) public void sendActivationEmail( + @ApiParam(value = "Email of the user", required = true) @RequestParam(value = "email") String email, HttpServletRequest request) throws ThingsboardException { try { @@ -214,10 +258,14 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Get the activation link (getActivationLink)", + notes = "Get the activation link for the user. " + + "The base url for activation link is configurable in the general settings of system administrator. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/{userId}/activationLink", method = RequestMethod.GET, produces = "text/plain") @ResponseBody public String getActivationLink( + @ApiParam(value = USER_ID_PARAM_DESCRIPTION) @PathVariable(USER_ID) String strUserId, HttpServletRequest request) throws ThingsboardException { checkParameter(USER_ID, strUserId); @@ -239,10 +287,15 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Delete User (deleteUser)", + notes = "Deletes the User, it's credentials and all the relations (from and to the User). " + + "Referencing non-existing User Id will cause an error. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteUser(@PathVariable(USER_ID) String strUserId) throws ThingsboardException { + public void deleteUser( + @ApiParam(value = USER_ID_PARAM_DESCRIPTION) + @PathVariable(USER_ID) String strUserId) throws ThingsboardException { checkParameter(USER_ID, strUserId); try { UserId userId = new UserId(toUUID(strUserId)); @@ -267,14 +320,22 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Get Users (getUsers)", + notes = "Returns a page of users owned by tenant or customer. The scope depends on authority of the user that performs the request." + + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getUsers( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); @@ -289,15 +350,23 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Get Tenant Users (getTenantAdmins)", + notes = "Returns a page of users owned by tenant. " + PAGE_DATA_PARAMETERS + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getTenantAdmins( - @PathVariable("tenantId") String strTenantId, + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(TENANT_ID) String strTenantId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("tenantId", strTenantId); try { @@ -309,15 +378,23 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Get Customer Users (getCustomerUsers)", + notes = "Returns a page of users owned by customer. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/{customerId}/users", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getCustomerUsers( - @PathVariable("customerId") String strCustomerId, + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) + @PathVariable(CUSTOMER_ID) String strCustomerId, + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = USER_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = USER_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { @@ -331,11 +408,16 @@ public class UserController extends BaseController { } } + @ApiOperation(value = "Enable/Disable User credentials (setUserCredentialsEnabled)", + notes = "Enables or Disables user credentials. Useful when you would like to block user account without deleting it. " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/user/{userId}/userCredentialsEnabled", method = RequestMethod.POST) @ResponseBody - public void setUserCredentialsEnabled(@PathVariable(USER_ID) String strUserId, - @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { + public void setUserCredentialsEnabled( + @ApiParam(value = USER_ID_PARAM_DESCRIPTION) + @PathVariable(USER_ID) String strUserId, + @ApiParam(value = "Disable (\"true\") or enable (\"false\") the credentials.", defaultValue = "true") + @RequestParam(required = false, defaultValue = "true") boolean userCredentialsEnabled) throws ThingsboardException { checkParameter(USER_ID, strUserId); try { UserId userId = new UserId(toUUID(strUserId)); 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 ad81821a82..a899f4cce2 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -26,7 +28,6 @@ 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.EntityType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; @@ -42,16 +43,30 @@ 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.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_ID_PARAM_DESCRIPTION; + @Slf4j @RestController @TbCoreComponent @RequestMapping("/api") public class WidgetTypeController extends BaseController { + 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"; + + + @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) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.GET) @ResponseBody - public WidgetTypeDetails getWidgetTypeById(@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { + public WidgetTypeDetails getWidgetTypeById( + @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { checkParameter("widgetTypeId", strWidgetTypeId); try { WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); @@ -61,10 +76,21 @@ public class WidgetTypeController extends BaseController { } } + @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 [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "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. " + + "Special Tenant Id '13814000-1dd2-11b2-8080-808080808080' is automatically used if the create request is sent by user with 'SYS_ADMIN' authority." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/widgetType", method = RequestMethod.POST) @ResponseBody - public WidgetTypeDetails saveWidgetType(@RequestBody WidgetTypeDetails widgetTypeDetails) throws ThingsboardException { + public WidgetTypeDetails saveWidgetType( + @ApiParam(value = "A JSON value representing the Widget Type Details.", required = true) + @RequestBody WidgetTypeDetails widgetTypeDetails) throws ThingsboardException { try { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID); @@ -84,10 +110,14 @@ public class WidgetTypeController extends BaseController { } } + @ApiOperation(value = "Delete widget type (deleteWidgetType)", + notes = "Deletes the Widget Type. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteWidgetType(@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { + public void deleteWidgetType( + @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException { checkParameter("widgetTypeId", strWidgetTypeId); try { WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); @@ -101,11 +131,15 @@ public class WidgetTypeController extends BaseController { } } + @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 = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @ResponseBody public List getBundleWidgetTypes( + @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, + @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias) throws ThingsboardException { try { TenantId tenantId; @@ -120,11 +154,15 @@ public class WidgetTypeController extends BaseController { } } + @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 = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @ResponseBody public List getBundleWidgetTypesDetails( + @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, + @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias) throws ThingsboardException { try { TenantId tenantId; @@ -139,11 +177,15 @@ public class WidgetTypeController extends BaseController { } } + @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')") @RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET) @ResponseBody public List getBundleWidgetTypesInfos( + @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, + @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias) throws ThingsboardException { try { TenantId tenantId; @@ -158,12 +200,17 @@ public class WidgetTypeController extends BaseController { } } + @ApiOperation(value = "Get Widget Type (getWidgetType)", + 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( + @ApiParam(value = "System or Tenant", required = true) @RequestParam boolean isSystem, + @ApiParam(value = "Widget Bundle alias", required = true) @RequestParam String bundleAlias, + @ApiParam(value = "Widget Type alias", required = true) @RequestParam String alias) throws ThingsboardException { try { TenantId tenantId; 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 14c5d6a860..a6cc14dce1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.controller; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -39,15 +41,33 @@ 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.WIDGET_BUNDLE_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION; + @RestController @TbCoreComponent @RequestMapping("/api") public class WidgetsBundleController extends BaseController { + private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. "; + + @ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)", + notes = "Get the Widget Bundle based on the provided Widget Bundle Id. " + WIDGET_BUNDLE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.GET) @ResponseBody - public WidgetsBundle getWidgetsBundleById(@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + public WidgetsBundle getWidgetsBundleById( + @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { checkParameter("widgetsBundleId", strWidgetsBundleId); try { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); @@ -57,10 +77,21 @@ public class WidgetsBundleController extends BaseController { } } + @ApiOperation(value = "Create Or Update Widget Bundle (saveWidgetsBundle)", + notes = "Create or update the Widget Bundle. " + WIDGET_BUNDLE_DESCRIPTION + " " + + "When creating the bundle, platform generates Widget Bundle Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " + + "The newly created Widget Bundle Id will be present in the response. " + + "Specify existing Widget Bundle id to update the Widget Bundle. " + + "Referencing non-existing Widget Bundle Id will cause 'Not Found' error." + + "\n\nWidget Bundle alias is unique in the scope of tenant. " + + "Special Tenant Id '13814000-1dd2-11b2-8080-808080808080' is automatically used if the create bundle request is sent by user with 'SYS_ADMIN' authority." + + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @RequestMapping(value = "/widgetsBundle", method = RequestMethod.POST) @ResponseBody - public WidgetsBundle saveWidgetsBundle(@RequestBody WidgetsBundle widgetsBundle) throws ThingsboardException { + public WidgetsBundle saveWidgetsBundle( + @ApiParam(value = "A JSON value representing the Widget Bundle.", required = true) + @RequestBody WidgetsBundle widgetsBundle) throws ThingsboardException { try { if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) { widgetsBundle.setTenantId(TenantId.SYS_TENANT_ID); @@ -80,10 +111,14 @@ public class WidgetsBundleController extends BaseController { } } + @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')") @RequestMapping(value = "/widgetsBundle/{widgetsBundleId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) - public void deleteWidgetsBundle(@PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { + public void deleteWidgetsBundle( + @ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true) + @PathVariable("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException { checkParameter("widgetsBundleId", strWidgetsBundleId); try { WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId)); @@ -97,14 +132,22 @@ public class WidgetsBundleController extends BaseController { } } + @ApiOperation(value = "Get Widget Bundles (getWidgetsBundles)", + notes = "Returns a page of Widget Bundle objects available for current user. " + WIDGET_BUNDLE_DESCRIPTION + " " + + PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetsBundles", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getWidgetsBundles( + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, + @ApiParam(value = WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES) @RequestParam(required = false) String sortProperty, + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) @RequestParam(required = false) String sortOrder) throws ThingsboardException { try { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); @@ -119,6 +162,8 @@ public class WidgetsBundleController extends BaseController { } } + @ApiOperation(value = "Get all Widget Bundles (getWidgetsBundles)", + notes = "Returns an array of Widget Bundle objects that are available for current user." + WIDGET_BUNDLE_DESCRIPTION + " " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/widgetsBundles", method = RequestMethod.GET) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 82d4dc5571..6e31fa1d73 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -63,6 +63,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; @Service @TbCoreComponent @@ -71,6 +73,8 @@ public class DeviceBulkImportService extends AbstractBulkImportService { protected final DeviceCredentialsService deviceCredentialsService; protected final DeviceProfileService deviceProfileService; + private final Lock findOrCreateDeviceProfileLock = new ReentrantLock(); + public DeviceBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache, AccessControlService accessControlService, AccessValidator accessValidator, EntityActionService entityActionService, TbClusterService clusterService, @@ -106,9 +110,15 @@ public class DeviceBulkImportService extends AbstractBulkImportService { throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); } + DeviceProfile deviceProfile; if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { - setUpLwM2mDeviceProfile(user.getTenantId(), device); + deviceProfile = setUpLwM2mDeviceProfile(user.getTenantId(), device); + } else if (StringUtils.isNotEmpty(device.getType())) { + deviceProfile = deviceProfileService.findOrCreateDeviceProfile(user.getTenantId(), device.getType()); + } else { + deviceProfile = deviceProfileService.findDefaultDeviceProfile(user.getTenantId()); } + device.setDeviceProfileId(deviceProfile.getId()); device = deviceService.saveDeviceWithCredentials(device, deviceCredentials); @@ -215,36 +225,43 @@ public class DeviceBulkImportService extends AbstractBulkImportService { credentials.setCredentialsValue(lwm2mCredentials.toString()); } - private void setUpLwM2mDeviceProfile(TenantId tenantId, Device device) { + private DeviceProfile setUpLwM2mDeviceProfile(TenantId tenantId, Device device) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileByName(tenantId, device.getType()); if (deviceProfile != null) { if (deviceProfile.getTransportType() != DeviceTransportType.LWM2M) { deviceProfile.setTransportType(DeviceTransportType.LWM2M); deviceProfile.getProfileData().setTransportConfiguration(new Lwm2mDeviceProfileTransportConfiguration()); deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); - device.setDeviceProfileId(deviceProfile.getId()); } } else { - deviceProfile = new DeviceProfile(); - deviceProfile.setTenantId(tenantId); - deviceProfile.setType(DeviceProfileType.DEFAULT); - deviceProfile.setName(device.getType()); - deviceProfile.setTransportType(DeviceTransportType.LWM2M); - deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); + findOrCreateDeviceProfileLock.lock(); + try { + deviceProfile = deviceProfileService.findDeviceProfileByName(tenantId, device.getType()); + if (deviceProfile == null) { + deviceProfile = new DeviceProfile(); + deviceProfile.setTenantId(tenantId); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setName(device.getType()); + deviceProfile.setTransportType(DeviceTransportType.LWM2M); + deviceProfile.setProvisionType(DeviceProfileProvisionType.DISABLED); - DeviceProfileData deviceProfileData = new DeviceProfileData(); - DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); - DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); - DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null); + DeviceProfileData deviceProfileData = new DeviceProfileData(); + DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); + DeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); + DisabledDeviceProfileProvisionConfiguration provisionConfiguration = new DisabledDeviceProfileProvisionConfiguration(null); - deviceProfileData.setConfiguration(configuration); - deviceProfileData.setTransportConfiguration(transportConfiguration); - deviceProfileData.setProvisionConfiguration(provisionConfiguration); - deviceProfile.setProfileData(deviceProfileData); + deviceProfileData.setConfiguration(configuration); + deviceProfileData.setTransportConfiguration(transportConfiguration); + deviceProfileData.setProvisionConfiguration(provisionConfiguration); + deviceProfile.setProfileData(deviceProfileData); - deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); - device.setDeviceProfileId(deviceProfile.getId()); + deviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); + } + } finally { + findOrCreateDeviceProfileLock.unlock(); + } } + return deviceProfile; } private void setValues(ObjectNode objectNode, Map data, Collection columns) { diff --git a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java index b1d0b30c9d..bbd18a1f73 100644 --- a/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/importing/AbstractBulkImportService.java @@ -22,6 +22,11 @@ import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.TenantProfile; @@ -47,11 +52,16 @@ import org.thingsboard.server.utils.CsvUtils; import org.thingsboard.server.utils.TypeCastUtil; import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -67,39 +77,53 @@ public abstract class AbstractBulkImportService processBulkImport(BulkImportRequest request, SecurityUser user, Consumer> onEntityImported) throws Exception { - BulkImportResult result = new BulkImportResult<>(); + private static ThreadPoolExecutor executor; - AtomicInteger i = new AtomicInteger(0); - if (request.getMapping().getHeader()) { - i.incrementAndGet(); + @PostConstruct + private void initExecutor() { + if (executor == null) { + executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().availableProcessors(), + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(150_000), + ThingsBoardThreadFactory.forName("bulk-import"), new ThreadPoolExecutor.CallerRunsPolicy()); + executor.allowCoreThreadTimeOut(true); } + } - parseData(request).forEach(entityData -> { - i.incrementAndGet(); - try { - ImportedEntityInfo importedEntityInfo = saveEntity(request, entityData.getFields(), user); - onEntityImported.accept(importedEntityInfo); + public final BulkImportResult processBulkImport(BulkImportRequest request, SecurityUser user, Consumer> onEntityImported) throws Exception { + List entitiesData = parseData(request); - E entity = importedEntityInfo.getEntity(); + BulkImportResult result = new BulkImportResult<>(); + CountDownLatch completionLatch = new CountDownLatch(entitiesData.size()); - saveKvs(user, entity, entityData.getKvs()); + SecurityContext securityContext = SecurityContextHolder.getContext(); - if (importedEntityInfo.getRelatedError() != null) { - throw new RuntimeException(importedEntityInfo.getRelatedError()); - } + entitiesData.forEach(entityData -> DonAsynchron.submit(() -> { + SecurityContextHolder.setContext(securityContext); - if (importedEntityInfo.isUpdated()) { - result.setUpdated(result.getUpdated() + 1); - } else { - result.setCreated(result.getCreated() + 1); - } - } catch (Exception e) { - result.setErrors(result.getErrors() + 1); - result.getErrorsList().add(String.format("Line %d: %s", i.get(), e.getMessage())); - } - }); + ImportedEntityInfo importedEntityInfo = saveEntity(request, entityData.getFields(), user); + E entity = importedEntityInfo.getEntity(); + + onEntityImported.accept(importedEntityInfo); + saveKvs(user, entity, entityData.getKvs()); + + return importedEntityInfo; + }, + importedEntityInfo -> { + if (importedEntityInfo.isUpdated()) { + result.getUpdated().incrementAndGet(); + } else { + result.getCreated().incrementAndGet(); + } + completionLatch.countDown(); + }, + throwable -> { + result.getErrors().incrementAndGet(); + result.getErrorsList().add(String.format("Line %d: %s", entityData.getLineNumber(), ExceptionUtils.getRootCauseMessage(throwable))); + completionLatch.countDown(); + }, + executor)); + completionLatch.await(); return result; } @@ -186,8 +210,11 @@ public abstract class AbstractBulkImportService parseData(BulkImportRequest request) throws Exception { List> records = CsvUtils.parseCsv(request.getFile(), request.getMapping().getDelimiter()); + AtomicInteger linesCounter = new AtomicInteger(0); + if (request.getMapping().getHeader()) { records.remove(0); + linesCounter.incrementAndGet(); } List columnsMappings = request.getMapping().getColumns(); @@ -205,15 +232,24 @@ public abstract class AbstractBulkImportService fields = new LinkedHashMap<>(); private final Map kvs = new LinkedHashMap<>(); + private int lineNumber; } @Data diff --git a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java b/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java index d6fa6ccbf9..7e937d835d 100644 --- a/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java +++ b/application/src/main/java/org/thingsboard/server/service/importing/BulkImportResult.java @@ -17,14 +17,14 @@ package org.thingsboard.server.service.importing; import lombok.Data; -import java.util.LinkedList; -import java.util.List; +import java.util.Collection; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; @Data public class BulkImportResult { - private int created = 0; - private int updated = 0; - private int errors = 0; - private List errorsList = new LinkedList<>(); - + private AtomicInteger created = new AtomicInteger(); + private AtomicInteger updated = new AtomicInteger(); + private AtomicInteger errors = new AtomicInteger(); + private Collection errorsList = new ConcurrentLinkedDeque<>(); } diff --git a/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java b/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java index 958863537a..846444c1d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java +++ b/application/src/main/java/org/thingsboard/server/service/importing/ImportedEntityInfo.java @@ -22,5 +22,4 @@ public class ImportedEntityInfo { private E entity; private boolean isUpdated; private E oldEntity; - private String relatedError; } 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 6677888854..0a82994a42 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 @@ -196,6 +196,13 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushNotificationToTransport(String serviceId, ToTransportMsg response, TbQueueCallback callback) { + if (serviceId == null || serviceId.isEmpty()){ + log.trace("pushNotificationToTransport: skipping message without serviceId [{}], (ToTransportMsg) response [{}]", serviceId, response); + if (callback != null) { + callback.onSuccess(null); //callback that message already sent, no useful payload expected + } + return; + } TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, serviceId); log.trace("PUSHING msg: {} to:{}", response, tpi); producerProvider.getTransportNotificationsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), response), callback); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index eca8864fda..e0d2cca803 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -87,6 +87,10 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi @Override public void sendRpcReplyToDevice(String serviceId, UUID sessionId, int requestId, String body) { + if (serviceId == null || serviceId.isEmpty()){ + log.trace("sendRpcReplyToDevice: skipping message without serviceId [{}], sessionId[{}], requestId[{}], body[{}]", serviceId, sessionId, requestId, body); + return; + } TransportProtos.ToServerRpcResponseMsg responseMsg = TransportProtos.ToServerRpcResponseMsg.newBuilder() .setRequestId(requestId) .setPayload(body).build(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java new file mode 100644 index 0000000000..97bf1d8b2d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ActivateUserRequest.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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.security.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel +@Data +public class ActivateUserRequest { + + @ApiModelProperty(position = 1, value = "The activate token to verify", example = "AAB254FF67D..") + private String activateToken; + @ApiModelProperty(position = 2, value = "The new password to set", example = "secret") + private String password; +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java new file mode 100644 index 0000000000..7875692fc4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ChangePasswordRequest.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2021 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.security.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel +@Data +public class ChangePasswordRequest { + + @ApiModelProperty(position = 1, value = "The old password", example = "OldPassword") + private String currentPassword; + @ApiModelProperty(position = 1, value = "The new password", example = "NewPassword") + private String newPassword; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java new file mode 100644 index 0000000000..e3f0b7d573 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2021 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.security.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +@ApiModel(value = "JWT Token Pair") +@Data +@AllArgsConstructor +public class JwtTokenPair { + + @ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..") + private String token; + @ApiModelProperty(position = 1, value = "The JWT Refresh Token. Used to get new JWT Access Token if old one has expired.", example = "AAB254FF67D..") + private String refreshToken; +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java new file mode 100644 index 0000000000..c668dab7ae --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordEmailRequest.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2021 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.security.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel +@Data +public class ResetPasswordEmailRequest { + + @ApiModelProperty(position = 1, value = "The email of the user", example = "user@example.com") + private String email; +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java new file mode 100644 index 0000000000..3d457a7937 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/ResetPasswordRequest.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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.security.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel +@Data +public class ResetPasswordRequest { + + @ApiModelProperty(position = 1, value = "The reset token to verify", example = "AAB254FF67D..") + private String resetToken; + @ApiModelProperty(position = 2, value = "The new password to set", example = "secret") + private String password; +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 893d7ed91c..c3343effb5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -177,7 +177,9 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { alarm.getLatest().computeIfAbsent(keyType, tmp -> new HashMap<>()).putAll(latestUpdate); return alarm; }).collect(Collectors.toList()); - wsService.sendWsMsg(sessionId, new AlarmDataUpdate(cmdId, null, update, maxEntitiesPerAlarmSubscription, data.getTotalElements())); + if (!update.isEmpty()) { + wsService.sendWsMsg(sessionId, new AlarmDataUpdate(cmdId, null, update, maxEntitiesPerAlarmSubscription, data.getTotalElements())); + } } else { log.trace("[{}][{}][{}][{}] Received stale subscription update: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), keyType, subscriptionUpdate); } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java index 0ee615d048..56ce42e1c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/AttributeData.java @@ -15,6 +15,10 @@ */ package org.thingsboard.server.service.telemetry; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel public class AttributeData implements Comparable{ private final long lastUpdateTs; @@ -28,14 +32,17 @@ public class AttributeData implements Comparable{ this.value = value; } + @ApiModelProperty(position = 1, value = "Timestamp last updated attribute, in milliseconds", example = "1609459200000", readOnly = true) public long getLastUpdateTs() { return lastUpdateTs; } + @ApiModelProperty(position = 2, value = "String representing attribute key", example = "active", readOnly = true) public String getKey() { return key; } + @ApiModelProperty(position = 3, value = "Object representing value of attribute key", example = "false", readOnly = true) public Object getValue() { return value; } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java b/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java index 3f8a1fffca..3b23d7cff1 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/TsData.java @@ -15,6 +15,10 @@ */ package org.thingsboard.server.service.telemetry; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel public class TsData implements Comparable{ private final long ts; @@ -26,10 +30,12 @@ public class TsData implements Comparable{ this.value = value; } + @ApiModelProperty(position = 1, value = "Timestamp last updated timeseries, in milliseconds", example = "1609459200000", readOnly = true) public long getTs() { return ts; } + @ApiModelProperty(position = 2, value = "Object representing value of timeseries key", example = "20", readOnly = true) public Object getValue() { return value; } diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java index 95f7e72ad7..b4101edc8e 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTbCoreToTransportService.java @@ -54,6 +54,13 @@ public class DefaultTbCoreToTransportService implements TbCoreToTransportService @Override public void process(String nodeId, ToTransportMsg msg, Runnable onSuccess, Consumer onFailure) { + if (nodeId == null || nodeId.isEmpty()){ + log.trace("process: skipping message without nodeId [{}], (ToTransportMsg) msg [{}]", nodeId, msg); + if (onSuccess != null) { + onSuccess.run(); + } + return; + } TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, nodeId); UUID sessionId = new UUID(msg.getSessionIdMSB(), msg.getSessionIdLSB()); log.trace("[{}][{}] Pushing session data to topic: {}", tpi.getFullTopicName(), sessionId, msg); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 61df401a94..83ab78f9c5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -129,10 +129,16 @@ usage: check: cycle: "${USAGE_STATS_CHECK_CYCLE:60000}" -# Dashboard parameters -dashboard: - # Maximum allowed datapoints fetched by widgets - max_datapoints_limit: "${DASHBOARD_MAX_DATAPOINTS_LIMIT:50000}" +# UI parameters +ui: + # Dashboard parameters + dashboard: + # Maximum allowed datapoints fetched by widgets + max_datapoints_limit: "${DASHBOARD_MAX_DATAPOINTS_LIMIT:50000}" + # Help parameters + help: + # Base url for UI help assets + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.3.2}" database: ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records @@ -279,6 +285,8 @@ sql: rpc: enabled: "${SQL_TTL_RPC_ENABLED:true}" checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours + relations: + max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # //This value has to be reasonable small to prevent infinite recursion as early as possible # Actor system parameters actors: @@ -704,6 +712,9 @@ transport: parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" # to configure SNMP to work over UDP or TCP underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" + stats: + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" # Edges parameters edges: diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java index bfe9176b4d..c54e76e4a9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java @@ -52,7 +52,17 @@ public abstract class AbstractMqttAttributesRequestIntegrationTest extends Abstr @Test public void testRequestAttributesValuesFromTheServer() throws Exception { - processTestRequestAttributesValuesFromTheServer(); + processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortTopic() throws Exception { + processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortJsonTopic() throws Exception { + processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); } @Test @@ -60,18 +70,18 @@ public abstract class AbstractMqttAttributesRequestIntegrationTest extends Abstr processTestGatewayRequestAttributesValuesFromTheServer(); } - protected void processTestRequestAttributesValuesFromTheServer() throws Exception { + protected void processTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { MqttAsyncClient client = getMqttAsyncClient(accessToken); - postAttributesAndSubscribeToTopic(savedDevice, client); + postAttributesAndSubscribeToTopic(savedDevice, client, attrPubTopic, attrSubTopic); Thread.sleep(5000); TestMqttCallback callback = getTestMqttCallback(); client.setCallback(callback); - validateResponse(client, callback.getLatch(), callback); + validateResponse(client, callback.getLatch(), callback, attrReqTopicPrefix); } protected void processTestGatewayRequestAttributesValuesFromTheServer() throws Exception { @@ -103,10 +113,10 @@ public abstract class AbstractMqttAttributesRequestIntegrationTest extends Abstr validateSharedResponseGateway(client, sharedAttributesCallback); } - protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception { + protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client, String attrPubTopic, String attrSubTopic) throws Exception { doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(POST_ATTRIBUTES_PAYLOAD.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + client.publish(attrPubTopic, new MqttMessage(POST_ATTRIBUTES_PAYLOAD.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); } protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { @@ -114,12 +124,12 @@ public abstract class AbstractMqttAttributesRequestIntegrationTest extends Abstr client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); } - protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback, String attrReqTopicPrefix) throws MqttException, InterruptedException, InvalidProtocolBufferException { String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; String payloadStr = "{\"clientKeys\":\"" + keys + "\", \"sharedKeys\":\"" + keys + "\"}"; MqttMessage mqttMessage = new MqttMessage(); mqttMessage.setPayload(payloadStr.getBytes()); - client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + client.publish(attrReqTopicPrefix + "1", mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); latch.await(1, TimeUnit.MINUTES); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); String expectedRequestPayload = "{\"client\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}},\"shared\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java index c805572e3d..2bd1877ee1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; @Slf4j public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { @@ -36,7 +37,17 @@ public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends A @Test public void testRequestAttributesValuesFromTheServer() throws Exception { - processTestRequestAttributesValuesFromTheServer(); + super.testRequestAttributesValuesFromTheServer(); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortTopic() throws Exception { + super.testRequestAttributesValuesFromTheServerOnShortTopic(); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortJsonTopic() throws Exception { + super.testRequestAttributesValuesFromTheServerOnShortJsonTopic(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java index 6949a544de..3015d91ffb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java @@ -84,7 +84,21 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends public void testRequestAttributesValuesFromTheServer() throws Exception { super.processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); - processTestRequestAttributesValuesFromTheServer(); + processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortTopic() throws Exception { + super.processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); + processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortProtoTopic() throws Exception { + super.processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED); + processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX); } @Test @@ -93,7 +107,10 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends processTestGatewayRequestAttributesValuesFromTheServer(); } - protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception { + @Test + public void testRequestAttributesValuesFromTheServerOnShortJsonTopic() throws Exception { } + + protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client, String attrPubTopic, String attrSubTopic) throws Exception { doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", AbstractMqttAttributesIntegrationTest.POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); @@ -131,8 +148,8 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends .setField(postAttributesMsgDescriptor.findFieldByName("attribute5"), jsonObject) .build(); byte[] payload = postAttributesMsg.toByteArray(); - client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(payload)); - client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + client.publish(attrPubTopic, new MqttMessage(payload)); + client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); } protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { @@ -149,7 +166,7 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes)); } - protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback, String attrReqTopic) throws MqttException, InterruptedException, InvalidProtocolBufferException { String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder(); attributesRequestBuilder.setClientKeys(keys); @@ -157,7 +174,7 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); MqttMessage mqttMessage = new MqttMessage(); mqttMessage.setPayload(attributesRequest.toByteArray()); - client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage); + client.publish(attrReqTopic + "1", mqttMessage); latch.await(3, TimeUnit.SECONDS); assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java index 61c5a25303..f179741abb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java @@ -61,7 +61,17 @@ public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends Abstr @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processTestSubscribeToAttributesUpdates(); + processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { + processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { + processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test @@ -69,14 +79,14 @@ public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends Abstr processGatewayTestSubscribeToAttributesUpdates(); } - protected void processTestSubscribeToAttributesUpdates() throws Exception { + protected void processTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { MqttAsyncClient client = getMqttAsyncClient(accessToken); TestMqttCallback onUpdateCallback = getTestMqttCallback(); client.setCallback(onUpdateCallback); - client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); Thread.sleep(1000); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java index 54b2abccc3..4efadaccaa 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java @@ -36,7 +36,17 @@ public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends A @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processTestSubscribeToAttributesUpdates(); + super.testSubscribeToAttributesUpdatesFromTheServer(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { + super.testSubscribeToAttributesUpdatesFromTheServerOnShortTopic(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { + super.testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic(); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java index 6c2565a016..606e05930d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; @@ -46,7 +47,20 @@ public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends @Test public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processTestSubscribeToAttributesUpdates(); + super.testSubscribeToAttributesUpdatesFromTheServer(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { + super.testSubscribeToAttributesUpdatesFromTheServerOnShortTopic(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception {} + + @Test + public void testSubscribeToAttributesUpdatesFromTheServerOnShortProtoTopic() throws Exception { + processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java index ea19ac6835..9be3d1d2ca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.service.security.AccessValidator; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -81,12 +82,32 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab @Test public void testServerMqttOneWayRpc() throws Exception { - processOneWayRpcTest(); + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortTopic() throws Exception { + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortJsonTopic() throws Exception { + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test public void testServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTest(); + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortJsonTopic() throws Exception { + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index e0ad7f79fc..1b468194b6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -60,14 +60,14 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM asyncContextTimeoutToUseRpcPlugin = 10000L; } - protected void processOneWayRpcTest() throws Exception { + protected void processOneWayRpcTest(String rpcSubTopic) throws Exception { MqttAsyncClient client = getMqttAsyncClient(accessToken); CountDownLatch latch = new CountDownLatch(1); TestMqttCallback callback = new TestMqttCallback(client, latch); client.setCallback(callback); - client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + client.subscribe(rpcSubTopic, MqttQoS.AT_MOST_ONCE.value()); Thread.sleep(1000); @@ -86,9 +86,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); } - protected void processTwoWayRpcTest() throws Exception { + protected void processTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttAsyncClient client = getMqttAsyncClient(accessToken); - client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + client.subscribe(rpcSubTopic, 1); CountDownLatch latch = new CountDownLatch(1); TestMqttCallback callback = new TestMqttCallback(client, latch); @@ -199,7 +199,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { MqttMessage message = new MqttMessage(); - if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { message.setPayload(DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8)); } else { JsonNode requestMsgNode = JacksonUtil.toJsonNode(new String(mqttMessage.getPayload(), StandardCharset.UTF_8)); @@ -232,7 +232,12 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM @Override public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); - String responseTopic = requestTopic.replace("request", "response"); + String responseTopic; + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { + responseTopic = requestTopic.replace("req", "res"); + } else { + responseTopic = requestTopic.replace("request", "response"); + } qoS = mqttMessage.getQos(); client.publish(responseTopic, processMessageArrived(requestTopic, mqttMessage)); latch.countDown(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java index 4b2f38cbd9..f8c0cd8bed 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; @Slf4j public abstract class AbstractMqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { @@ -37,12 +38,32 @@ public abstract class AbstractMqttServerSideRpcJsonIntegrationTest extends Abstr @Test public void testServerMqttOneWayRpc() throws Exception { - processOneWayRpcTest(); + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortTopic() throws Exception { + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortJsonTopic() throws Exception { + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test public void testServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTest(); + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortJsonTopic() throws Exception { + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java index 3792d66ac4..2c9f1f7889 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java @@ -78,12 +78,32 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst @Test public void testServerMqttOneWayRpc() throws Exception { - processOneWayRpcTest(); + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortTopic() throws Exception { + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortProtoTopic() throws Exception { + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test public void testServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTest(); + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortProtoTopic() throws Exception { + processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test @@ -118,9 +138,9 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst return builder.build(); } - protected void processTwoWayRpcTest() throws Exception { + protected void processTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttAsyncClient client = getMqttAsyncClient(accessToken); - client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1); + client.subscribe(rpcSubTopic, 1); CountDownLatch latch = new CountDownLatch(1); TestMqttCallback callback = new TestMqttCallback(client, latch); @@ -139,7 +159,7 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { MqttMessage message = new MqttMessage(); - if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) { + if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = getProtoTransportPayloadConfiguration(); ProtoFileElement rpcRequestProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(RPC_REQUEST_PROTO_SCHEMA); DynamicSchema rpcRequestProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(rpcRequestProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_REQUEST_PROTO_SCHEMA); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java index 6a45ab5989..e6cf60fb2e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java @@ -60,6 +60,18 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); } + @Test + public void testPushAttributesOnShortTopic() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + } + + @Test + public void testPushAttributesOnShortJsonTopic() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); + } + @Test public void testPushAttributesGateway() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java index 345913a64f..9530bb2449 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesJsonIntegrationTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import java.util.Arrays; import java.util.List; @@ -45,12 +46,18 @@ public abstract class AbstractMqttAttributesJsonIntegrationTest extends Abstract processJsonPayloadAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes()); } + @Test + public void testPushAttributesOnShortTopic() throws Exception { + super.testPushAttributesOnShortTopic(); + } + + @Test + public void testPushAttributesOnShortJsonTopic() throws Exception { + super.testPushAttributesOnShortJsonTopic(); + } + @Test public void testPushAttributesGateway() throws Exception { - List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - String deviceName1 = "Device A"; - String deviceName2 = "Device B"; - String payload = getGatewayAttributesJsonPayload(deviceName1, deviceName2); - processGatewayAttributesTest(expectedKeys, payload.getBytes(), deviceName1, deviceName2); + super.testPushAttributesGateway(); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java index c77691981e..afe624c55b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/AbstractMqttAttributesProtoIntegrationTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.gen.transport.TransportApiProtos; @@ -49,14 +50,14 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac @Test public void testPushAttributes() throws Exception { super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); - DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - ProtoFileElement transportProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_ATTRIBUTES_PROTO_SCHEMA); - DynamicSchema attributesSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.ATTRIBUTES_PROTO_SCHEMA); + DynamicMessage postAttributesMsg = getDefaultDynamicMessage(); + processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); + } + + @Test + public void testPushAttributesWithExplicitPresenceProtoKeys() throws Exception { + super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); + DynamicSchema attributesSchema = getDynamicSchema(); DynamicMessage.Builder nestedJsonObjectBuilder = attributesSchema.newMessageBuilder("PostAttributes.JsonObject.NestedJsonObject"); Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); @@ -67,7 +68,6 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac Descriptors.Descriptor jsonObjectBuilderDescriptor = jsonObjectBuilder.getDescriptorForType(); assertNotNull(jsonObjectBuilderDescriptor); DynamicMessage jsonObject = jsonObjectBuilder - .setField(jsonObjectBuilderDescriptor.findFieldByName("someNumber"), 42) .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 1) .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 2) .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 3) @@ -78,18 +78,50 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac Descriptors.Descriptor postAttributesMsgDescriptor = postAttributesBuilder.getDescriptorForType(); assertNotNull(postAttributesMsgDescriptor); DynamicMessage postAttributesMsg = postAttributesBuilder - .setField(postAttributesMsgDescriptor.findFieldByName("key1"), "value1") - .setField(postAttributesMsgDescriptor.findFieldByName("key2"), true) - .setField(postAttributesMsgDescriptor.findFieldByName("key3"), 3.0) - .setField(postAttributesMsgDescriptor.findFieldByName("key4"), 4) + .setField(postAttributesMsgDescriptor.findFieldByName("key1"), "") + .setField(postAttributesMsgDescriptor.findFieldByName("key2"), false) + .setField(postAttributesMsgDescriptor.findFieldByName("key3"), 0.0) + .setField(postAttributesMsgDescriptor.findFieldByName("key4"), 0) .setField(postAttributesMsgDescriptor.findFieldByName("key5"), jsonObject) .build(); - processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); + processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), true); } @Test - public void testPushAttributesWithExplicitPresenceProtoKeys() throws Exception { + public void testPushAttributesOnShortTopic() throws Exception { super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); + DynamicMessage postAttributesMsg = getDefaultDynamicMessage(); + processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); + } + + @Test + public void testPushAttributesOnShortJsonTopic() throws Exception { + super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); + processJsonPayloadAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes()); + } + + @Test + public void testPushAttributesOnShortProtoTopic() throws Exception { + super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); + DynamicMessage postAttributesMsg = getDefaultDynamicMessage(); + processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); + } + + @Test + public void testPushAttributesGateway() throws Exception { + super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, null); + TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributesMsgProtoBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.AttributesMsg firstDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName1, expectedKeys); + TransportApiProtos.AttributesMsg secondDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName2, expectedKeys); + gatewayAttributesMsgProtoBuilder.addAllMsg(Arrays.asList(firstDeviceAttributesMsgProto, secondDeviceAttributesMsgProto)); + TransportApiProtos.GatewayAttributesMsg gatewayAttributesMsg = gatewayAttributesMsgProtoBuilder.build(); + processGatewayAttributesTest(expectedKeys, gatewayAttributesMsg.toByteArray(), deviceName1, deviceName2); + } + + private DynamicSchema getDynamicSchema() { DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; @@ -97,7 +129,11 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; ProtoFileElement transportProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_ATTRIBUTES_PROTO_SCHEMA); - DynamicSchema attributesSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.ATTRIBUTES_PROTO_SCHEMA); + return protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.ATTRIBUTES_PROTO_SCHEMA); + } + + private DynamicMessage getDefaultDynamicMessage() { + DynamicSchema attributesSchema = getDynamicSchema(); DynamicMessage.Builder nestedJsonObjectBuilder = attributesSchema.newMessageBuilder("PostAttributes.JsonObject.NestedJsonObject"); Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); @@ -108,6 +144,7 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac Descriptors.Descriptor jsonObjectBuilderDescriptor = jsonObjectBuilder.getDescriptorForType(); assertNotNull(jsonObjectBuilderDescriptor); DynamicMessage jsonObject = jsonObjectBuilder + .setField(jsonObjectBuilderDescriptor.findFieldByName("someNumber"), 42) .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 1) .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 2) .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 3) @@ -117,25 +154,13 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac DynamicMessage.Builder postAttributesBuilder = attributesSchema.newMessageBuilder("PostAttributes"); Descriptors.Descriptor postAttributesMsgDescriptor = postAttributesBuilder.getDescriptorForType(); assertNotNull(postAttributesMsgDescriptor); - DynamicMessage postAttributesMsg = postAttributesBuilder - .setField(postAttributesMsgDescriptor.findFieldByName("key1"), "") + return postAttributesBuilder + .setField(postAttributesMsgDescriptor.findFieldByName("key1"), "value1") + .setField(postAttributesMsgDescriptor.findFieldByName("key2"), true) + .setField(postAttributesMsgDescriptor.findFieldByName("key3"), 3.0) + .setField(postAttributesMsgDescriptor.findFieldByName("key4"), 4) .setField(postAttributesMsgDescriptor.findFieldByName("key5"), jsonObject) .build(); - processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, Arrays.asList("key1", "key5"), postAttributesMsg.toByteArray(), true); - } - - @Test - public void testPushAttributesGateway() throws Exception { - super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, null); - TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributesMsgProtoBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); - List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - String deviceName1 = "Device A"; - String deviceName2 = "Device B"; - TransportApiProtos.AttributesMsg firstDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName1, expectedKeys); - TransportApiProtos.AttributesMsg secondDeviceAttributesMsgProto = getDeviceAttributesMsgProto(deviceName2, expectedKeys); - gatewayAttributesMsgProtoBuilder.addAllMsg(Arrays.asList(firstDeviceAttributesMsgProto, secondDeviceAttributesMsgProto)); - TransportApiProtos.GatewayAttributesMsg gatewayAttributesMsg = gatewayAttributesMsgProtoBuilder.build(); - processGatewayAttributesTest(expectedKeys, gatewayAttributesMsg.toByteArray(), deviceName1, deviceName2); } private TransportApiProtos.AttributesMsg getDeviceAttributesMsgProto(String deviceName, List expectedKeys) { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 4bd8977abc..d7b4cc553e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -73,6 +73,18 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); } + @Test + public void testPushTelemetryOnShortTopic() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + } + + @Test + public void testPushTelemetryOnShortJsonTopic() throws Exception { + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes(), false); + } + @Test public void testPushTelemetryGateway() throws Exception { List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java index 1b6107ff71..6887941e4d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java @@ -57,25 +57,23 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract processJsonPayloadTelemetryTest(POST_DATA_TELEMETRY_TOPIC, expectedKeys, payloadStr.getBytes(), true); } + @Test + public void testPushTelemetryOnShortTopic() throws Exception { + super.testPushTelemetryOnShortTopic(); + } + + @Test + public void testPushTelemetryWithTsOnShortJsonTopic() throws Exception { + super.testPushTelemetryOnShortJsonTopic(); + } + @Test public void testPushTelemetryGateway() throws Exception { - List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); - String deviceName1 = "Device A"; - String deviceName2 = "Device B"; - String payload = getGatewayTelemetryJsonPayload(deviceName1, deviceName2, "10000", "20000"); - processGatewayTelemetryTest(MqttTopics.GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); + super.testPushTelemetryGateway(); } @Test public void testGatewayConnect() throws Exception { - String payload = "{\"device\":\"Device A\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - publishMqttMsg(client, payload.getBytes(), MqttTopics.GATEWAY_CONNECT_TOPIC); - - String deviceName = "Device A"; - Device device = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class), - 20, - 100); - assertNotNull(device); + super.testGatewayConnect(); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java index 9b0fa5dd33..a10e19a35c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -21,6 +21,7 @@ import com.google.protobuf.DynamicMessage; import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Ignore; import org.junit.Test; @@ -55,41 +56,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testPushTelemetry() throws Exception { super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); - DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_TELEMETRY_PROTO_SCHEMA); - DynamicSchema telemetrySchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, "telemetrySchema"); - - DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); - Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); - assertNotNull(nestedJsonObjectBuilderDescriptor); - DynamicMessage nestedJsonObject = nestedJsonObjectBuilder.setField(nestedJsonObjectBuilderDescriptor.findFieldByName("key"), "value").build(); - - DynamicMessage.Builder jsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject"); - Descriptors.Descriptor jsonObjectBuilderDescriptor = jsonObjectBuilder.getDescriptorForType(); - assertNotNull(jsonObjectBuilderDescriptor); - DynamicMessage jsonObject = jsonObjectBuilder - .setField(jsonObjectBuilderDescriptor.findFieldByName("someNumber"), 42) - .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 1) - .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 2) - .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 3) - .setField(jsonObjectBuilderDescriptor.findFieldByName("someNestedObject"), nestedJsonObject) - .build(); - - DynamicMessage.Builder postTelemetryBuilder = telemetrySchema.newMessageBuilder("PostTelemetry"); - Descriptors.Descriptor postTelemetryMsgDescriptor = postTelemetryBuilder.getDescriptorForType(); - assertNotNull(postTelemetryMsgDescriptor); - DynamicMessage postTelemetryMsg = postTelemetryBuilder - .setField(postTelemetryMsgDescriptor.findFieldByName("key1"), "value1") - .setField(postTelemetryMsgDescriptor.findFieldByName("key2"), true) - .setField(postTelemetryMsgDescriptor.findFieldByName("key3"), 3.0) - .setField(postTelemetryMsgDescriptor.findFieldByName("key4"), 4) - .setField(postTelemetryMsgDescriptor.findFieldByName("key5"), jsonObject) - .build(); + DynamicMessage postTelemetryMsg = getDefaultDynamicMessage(); processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); } @@ -121,14 +88,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac " }\n" + "}"; super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, schemaStr, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); - DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(schemaStr); - DynamicSchema telemetrySchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, "telemetrySchema"); + DynamicSchema telemetrySchema = getDynamicSchema(schemaStr); DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); @@ -173,14 +133,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testPushTelemetryWithExplicitPresenceProtoKeys() throws Exception { super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); - DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_TELEMETRY_PROTO_SCHEMA); - DynamicSchema telemetrySchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, "telemetrySchema"); + DynamicSchema telemetrySchema = getDynamicSchema(DEVICE_TELEMETRY_PROTO_SCHEMA); DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); @@ -237,14 +190,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac " }\n" + "}"; super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, schemaStr, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); - DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(schemaStr); - DynamicSchema telemetrySchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, "telemetrySchema"); + DynamicSchema telemetrySchema = getDynamicSchema(schemaStr); DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); @@ -281,6 +227,26 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), true, true); } + @Test + public void testPushTelemetryOnShortTopic() throws Exception { + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); + DynamicMessage postTelemetryMsg = getDefaultDynamicMessage(); + processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); + } + + @Test + public void testPushTelemetryOnShortJsonTopic() throws Exception { + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); + processJsonPayloadTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_JSON_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes(), false); + } + + @Test + public void testPushTelemetryOnShortProtoTopic() throws Exception { + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null); + DynamicMessage postTelemetryMsg = getDefaultDynamicMessage(); + processTelemetryTest(MqttTopics.DEVICE_TELEMETRY_SHORT_PROTO_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); + } + @Test public void testPushTelemetryGateway() throws Exception { super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); @@ -310,6 +276,48 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac assertNotNull(device); } + private DynamicSchema getDynamicSchema(String deviceTelemetryProtoSchema) { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); + MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(deviceTelemetryProtoSchema); + return protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, "telemetrySchema"); + } + + private DynamicMessage getDefaultDynamicMessage() { + DynamicSchema telemetrySchema = getDynamicSchema(DEVICE_TELEMETRY_PROTO_SCHEMA); + + DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); + Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); + assertNotNull(nestedJsonObjectBuilderDescriptor); + DynamicMessage nestedJsonObject = nestedJsonObjectBuilder.setField(nestedJsonObjectBuilderDescriptor.findFieldByName("key"), "value").build(); + + DynamicMessage.Builder jsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject"); + Descriptors.Descriptor jsonObjectBuilderDescriptor = jsonObjectBuilder.getDescriptorForType(); + assertNotNull(jsonObjectBuilderDescriptor); + DynamicMessage jsonObject = jsonObjectBuilder + .setField(jsonObjectBuilderDescriptor.findFieldByName("someNumber"), 42) + .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 1) + .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 2) + .addRepeatedField(jsonObjectBuilderDescriptor.findFieldByName("someArray"), 3) + .setField(jsonObjectBuilderDescriptor.findFieldByName("someNestedObject"), nestedJsonObject) + .build(); + + DynamicMessage.Builder postTelemetryBuilder = telemetrySchema.newMessageBuilder("PostTelemetry"); + Descriptors.Descriptor postTelemetryMsgDescriptor = postTelemetryBuilder.getDescriptorForType(); + assertNotNull(postTelemetryMsgDescriptor); + return postTelemetryBuilder + .setField(postTelemetryMsgDescriptor.findFieldByName("key1"), "value1") + .setField(postTelemetryMsgDescriptor.findFieldByName("key2"), true) + .setField(postTelemetryMsgDescriptor.findFieldByName("key3"), 3.0) + .setField(postTelemetryMsgDescriptor.findFieldByName("key4"), 4) + .setField(postTelemetryMsgDescriptor.findFieldByName("key5"), jsonObject) + .build(); + } + private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); builder.setDeviceName(deviceName); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java index ae994d4122..e089d7bf47 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.RuleNodeId; 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.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; @@ -71,7 +70,7 @@ public interface RuleChainService { RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException; - List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite); + List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite); RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId); diff --git a/common/data/pom.xml b/common/data/pom.xml index 3b9152fb47..1b5c78bcd6 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -96,6 +96,10 @@ org.apache.commons commons-lang3 + + io.swagger + swagger-annotations + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java index 356af5dab7..61328dd352 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java @@ -15,11 +15,15 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.id.AdminSettingsId; import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.validation.NoXss; +@ApiModel public class AdminSettings extends BaseData { private static final long serialVersionUID = -7670322981725511892L; @@ -42,6 +46,19 @@ public class AdminSettings extends BaseData { this.jsonValue = adminSettings.getJsonValue(); } + @ApiModelProperty(position = 1, value = "The Id of the Administration Settings, auto-generated, UUID") + @Override + public AdminSettingsId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the settings creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 3, value = "The Administration Settings key, (e.g. 'general' or 'mail')") public String getKey() { return key; } @@ -50,6 +67,7 @@ public class AdminSettings extends BaseData { this.key = key; } + @ApiModelProperty(position = 4, value = "JSON representation of the Administration Settings value") public JsonNode getJsonValue() { return jsonValue; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index f6f49bb33b..14e2ea73c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -18,6 +18,8 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.NoXss; @@ -27,7 +29,9 @@ public class Customer extends ContactBased implements HasTenantId { private static final long serialVersionUID = -1599722990298929275L; @NoXss + @ApiModelProperty(position = 3, value = "Title of the customer", example = "Company A") private String title; + @ApiModelProperty(position = 5, required = true, value = "JSON object with Tenant Id") private TenantId tenantId; public Customer() { @@ -51,7 +55,7 @@ public class Customer extends ContactBased implements HasTenantId { public void setTenantId(TenantId tenantId) { this.tenantId = tenantId; } - + public String getTitle() { return title; } @@ -60,6 +64,75 @@ public class Customer extends ContactBased implements HasTenantId { this.title = title; } + @ApiModelProperty(position = 1, value = "JSON object with the customer Id. " + + "Specify this field to update the customer. " + + "Referencing non-existing customer Id will cause error. " + + "Omit this field to create new customer." ) + @Override + public CustomerId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the customer creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 6, required = true, value = "Country", example = "US") + @Override + public String getCountry() { + return super.getCountry(); + } + + @ApiModelProperty(position = 7, required = true, value = "State", example = "NY") + @Override + public String getState() { + return super.getState(); + } + + @ApiModelProperty(position = 8, required = true, value = "City", example = "New York") + @Override + public String getCity() { + return super.getCity(); + } + + @ApiModelProperty(position = 9, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400") + @Override + public String getAddress() { + return super.getAddress(); + } + + @ApiModelProperty(position = 10, required = true, value = "Address Line 2", example = "") + @Override + public String getAddress2() { + return super.getAddress2(); + } + + @ApiModelProperty(position = 11, required = true, value = "Zip code", example = "10004") + @Override + public String getZip() { + return super.getZip(); + } + + @ApiModelProperty(position = 12, required = true, value = "Phone number", example = "+1(415)777-7777") + @Override + public String getPhone() { + return super.getPhone(); + } + + @ApiModelProperty(position = 13, required = true, value = "Email", example = "example@company.com") + @Override + public String getEmail() { + return super.getEmail(); + } + + @ApiModelProperty(position = 14, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + @JsonIgnore public boolean isPublic() { if (getAdditionalInfo() != null && getAdditionalInfo().has("isPublic")) { @@ -76,6 +149,7 @@ public class Customer extends ContactBased implements HasTenantId { @Override @JsonProperty(access = Access.READ_ONLY) + @ApiModelProperty(position = 4, value = "Name of the customer. Read-only, duplicated from title for backward compatibility", example = "Company A", readOnly = true) public String getName() { return title; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java index 7d8a3d5266..c9a95375ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.id.DashboardId; public class Dashboard extends DashboardInfo { @@ -41,6 +42,10 @@ public class Dashboard extends DashboardInfo { this.configuration = dashboard.getConfiguration(); } + @ApiModelProperty(position = 9, value = "JSON object with main configuration of the dashboard: layouts, widgets, aliases, etc. " + + "The JSON structure of the dashboard configuration is quite complex. " + + "The easiest way to learn it is to export existing dashboard to JSON." + , dataType = "com.fasterxml.jackson.databind.JsonNode") public JsonNode getConfiguration() { return configuration; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index b42e0069df..556f13d18e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; @@ -25,6 +27,7 @@ import javax.validation.Valid; import java.util.HashSet; import java.util.Set; +@ApiModel public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId { private TenantId tenantId; @@ -54,6 +57,22 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.mobileOrder = dashboardInfo.getMobileOrder(); } + @ApiModelProperty(position = 1, value = "JSON object with the dashboard Id. " + + "Specify existing dashboard Id to update the dashboard. " + + "Referencing non-existing dashboard id will cause error. " + + "Omit this field to create new dashboard." ) + @Override + public DashboardId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the dashboard creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id. Tenant Id of the dashboard can't be changed.", readOnly = true) public TenantId getTenantId() { return tenantId; } @@ -62,6 +81,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.tenantId = tenantId; } + @ApiModelProperty(position = 4, value = "Title of the dashboard.") public String getTitle() { return title; } @@ -70,6 +90,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.title = title; } + @ApiModelProperty(position = 8, value = "Thumbnail picture for rendering of the dashboards in a grid view on mobile devices.", readOnly = true) public String getImage() { return image; } @@ -78,6 +99,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.image = image; } + @ApiModelProperty(position = 5, value = "List of assigned customers with their info.", readOnly = true) public Set getAssignedCustomers() { return assignedCustomers; } @@ -86,6 +108,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.assignedCustomers = assignedCustomers; } + @ApiModelProperty(position = 6, value = "Hide dashboard from mobile devices. Useful if the dashboard is not designed for small screens.", readOnly = true) public boolean isMobileHide() { return mobileHide; } @@ -94,6 +117,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.mobileHide = mobileHide; } + @ApiModelProperty(position = 7, value = "Order on mobile devices. Useful to adjust sorting of the dashboards for mobile applications", readOnly = true) public Integer getMobileOrder() { return mobileOrder; } @@ -152,6 +176,7 @@ public class DashboardInfo extends SearchTextBased implements HasNa } } + @ApiModelProperty(position = 4, value = "Same as title of the dashboard. Read-only field. Update the 'title' to change the 'name' of the dashboard.", readOnly = true) @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index c5519d329b..a11c05f2b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -17,6 +17,9 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.device.data.DeviceData; @@ -31,6 +34,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.Optional; +@ApiModel @EqualsAndHashCode(callSuper = true) @Slf4j public class Device extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, HasOtaPackage { @@ -88,6 +92,22 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen return this; } + @ApiModelProperty(position = 1, value = "JSON object with the Device Id. " + + "Specify this field to update the Device. " + + "Referencing non-existing Device Id will cause error. " + + "Omit this field to create new Device." ) + @Override + public DeviceId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the device creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id. Use 'assignDeviceToTenant' to change the Tenant Id.", readOnly = true) public TenantId getTenantId() { return tenantId; } @@ -96,6 +116,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.tenantId = tenantId; } + @ApiModelProperty(position = 4, value = "JSON object with Customer Id. Use 'assignDeviceToCustomer' to change the Customer Id.", readOnly = true) public CustomerId getCustomerId() { return customerId; } @@ -104,6 +125,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.customerId = customerId; } + @ApiModelProperty(position = 5, required = true, value = "Unique Device Name in scope of Tenant", example = "A4B72CCDFF33") @Override public String getName() { return name; @@ -113,6 +135,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.name = name; } + @ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor") public String getType() { return type; } @@ -121,6 +144,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.type = type; } + @ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "Room 234 Sensor") public String getLabel() { return label; } @@ -129,6 +153,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.label = label; } + @ApiModelProperty(position = 8, required = true, value = "JSON object with Device Profile Id.") public DeviceProfileId getDeviceProfileId() { return deviceProfileId; } @@ -137,6 +162,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.deviceProfileId = deviceProfileId; } + @ApiModelProperty(position = 9, value = "JSON object with content specific to type of transport in the device profile.") public DeviceData getDeviceData() { if (deviceData != null) { return deviceData; @@ -169,6 +195,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen return getName(); } + @ApiModelProperty(position = 10, value = "JSON object with Ota Package Id.") public OtaPackageId getFirmwareId() { return firmwareId; } @@ -177,6 +204,7 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.firmwareId = firmwareId; } + @ApiModelProperty(position = 11, value = "JSON object with Ota Package Id.") public OtaPackageId getSoftwareId() { return softwareId; } @@ -185,6 +213,12 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen this.softwareId = softwareId; } + @ApiModelProperty(position = 12, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java index 7730ea635b..be872cc059 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceInfo.java @@ -15,14 +15,20 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.DeviceId; +@ApiModel @Data public class DeviceInfo extends Device { + @ApiModelProperty(position = 13, value = "Title of the Customer that owns the device.", readOnly = true) private String customerTitle; + @ApiModelProperty(position = 14, value = "Indicates special 'Public' Customer that is auto-generated to use the devices on public dashboards.", readOnly = true) private boolean customerIsPublic; + @ApiModelProperty(position = 15, value = "Name of the corresponding Device Profile.", readOnly = true) private String deviceProfileName; public DeviceInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java index 13c4692976..5e0419cf90 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfile.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @@ -34,34 +36,51 @@ import java.io.IOException; import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) @Slf4j public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId, HasOtaPackage { + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id that owns the profile.", readOnly = true) private TenantId tenantId; @NoXss + @ApiModelProperty(position = 4, value = "Unique Device Profile Name in scope of Tenant.", example = "Moisture Sensor") private String name; @NoXss + @ApiModelProperty(position = 11, value = "Device Profile description. ") private String description; + @ApiModelProperty(position = 12, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ") private String image; private boolean isDefault; + @ApiModelProperty(position = 16, value = "Type of the profile. Always 'DEFAULT' for now. Reserved for future use.") private DeviceProfileType type; + @ApiModelProperty(position = 14, value = "Type of the transport used to connect the device. Default transport supports HTTP, CoAP and MQTT.") private DeviceTransportType transportType; + @ApiModelProperty(position = 15, value = "Provisioning strategy.") private DeviceProfileProvisionType provisionType; + @ApiModelProperty(position = 7, value = "Reference to the rule chain. " + + "If present, the specified rule chain will be used to process all messages related to device, including telemetry, attribute updates, etc. " + + "Otherwise, the root rule chain will be used to process those messages.") private RuleChainId defaultRuleChainId; + @ApiModelProperty(position = 6, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to device details.") private DashboardId defaultDashboardId; @NoXss + @ApiModelProperty(position = 8, value = "Reference to the rule engine queue. " + + "If present, the specified queue will be used to store all unprocessed messages related to device, including telemetry, attribute updates, etc. " + + "Otherwise, the 'Main' queue will be used to store those messages.") private String defaultQueueName; @Valid private transient DeviceProfileData profileData; @JsonIgnore private byte[] profileDataBytes; @NoXss + @ApiModelProperty(position = 13, value = "Unique provisioning key used by 'Device Provisioning' feature.") private String provisionDeviceKey; + @ApiModelProperty(position = 9, value = "Reference to the firmware OTA package. If present, the specified package will be used as default device firmware. ") private OtaPackageId firmwareId; - + @ApiModelProperty(position = 10, value = "Reference to the software OTA package. If present, the specified package will be used as default device software. ") private OtaPackageId softwareId; public DeviceProfile() { @@ -88,16 +107,32 @@ public class DeviceProfile extends SearchTextBased implements H this.softwareId = deviceProfile.getSoftwareId(); } + @ApiModelProperty(position = 1, value = "JSON object with the device profile Id. " + + "Specify this field to update the device profile. " + + "Referencing non-existing device profile Id will cause error. " + + "Omit this field to create new device profile.") + @Override + public DeviceProfileId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the profile creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + @Override public String getSearchText() { return getName(); } - @Override - public String getName() { - return name; + @ApiModelProperty(position = 5, value = "Used to mark the default profile. Default profile is used when the device profile is not specified during device creation.") + public boolean isDefault(){ + return isDefault; } + @ApiModelProperty(position = 16, value = "Complex JSON object that includes addition device profile configuration (transport, alarm rules, etc).") public DeviceProfileData getProfileData() { if (profileData != null) { return profileData; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java index 7141382749..a35b281a46 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceProfileInfo.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.Value; @@ -31,9 +32,13 @@ import java.util.UUID; @ToString(callSuper = true) public class DeviceProfileInfo extends EntityInfo { + @ApiModelProperty(position = 3, value = "Either URL or Base64 data of the icon. Used in the mobile application to visualize set of device profiles in the grid view. ") private final String image; + @ApiModelProperty(position = 4, value = "Reference to the dashboard. Used in the mobile application to open the default dashboard when user navigates to device details.") private final DashboardId defaultDashboardId; + @ApiModelProperty(position = 5, value = "Type of the profile. Always 'DEFAULT' for now. Reserved for future use.") private final DeviceProfileType type; + @ApiModelProperty(position = 6, value = "Type of the transport used to connect the device. Default transport supports HTTP, CoAP and MQTT.") private final DeviceTransportType transportType; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java index 3162d66dbb..b551583503 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityInfo.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -24,10 +26,13 @@ import org.thingsboard.server.common.data.id.HasId; import java.util.UUID; +@ApiModel @Data public class EntityInfo implements HasId, HasName { + @ApiModelProperty(position = 1, value = "JSON object with the entity Id. ") private final EntityId id; + @ApiModelProperty(position = 2, value = "Entity Name") private final String name; @JsonCreator diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java index e8b48ee23a..7f5de9f945 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -37,15 +39,21 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo private static final long serialVersionUID = 5582010124562018986L; + @ApiModelProperty(position = 7, required = true, value = "JSON object with the referenced Entity Id (Device or Asset).") private EntityId entityId; private TenantId tenantId; private CustomerId customerId; @NoXss + @ApiModelProperty(position = 5, required = true, value = "Entity View name", example = "A4B72CCDFF33") private String name; @NoXss + @ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor") private String type; + @ApiModelProperty(position = 8, required = true, value = "Set of telemetry and attribute keys to expose via Entity View.") private TelemetryEntityView keys; + @ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;") private long startTimeMs; + @ApiModelProperty(position = 10, value = "Represents the end time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;") private long endTimeMs; public EntityView() { @@ -73,6 +81,7 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo return getName() /*What the ...*/; } + @ApiModelProperty(position = 4, value = "JSON object with Customer Id. Use 'assignEntityViewToCustomer' to change the Customer Id.", readOnly = true) @Override public CustomerId getCustomerId() { return customerId; @@ -83,8 +92,31 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo return name; } + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true) @Override public TenantId getTenantId() { return tenantId; } + + @ApiModelProperty(position = 1, value = "JSON object with the Entity View Id. " + + "Specify this field to update the Entity View. " + + "Referencing non-existing Entity View Id will cause error. " + + "Omit this field to create new Entity View." ) + @Override + public EntityViewId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the Entity View creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 11, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java index e2c42e7d0f..a3e027c468 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.EntityViewId; @Data public class EntityViewInfo extends EntityView { + @ApiModelProperty(position = 12, value = "Title of the Customer that owns the entity view.", readOnly = true) private String customerTitle; + @ApiModelProperty(position = 13, value = "Indicates special 'Public' Customer that is auto-generated to use the entity view on public dashboards.", readOnly = true) private boolean customerIsPublic; public EntityViewInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Event.java b/common/data/src/main/java/org/thingsboard/server/common/data/Event.java index f218ef9929..915bebfdb5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Event.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Event.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EventId; @@ -25,12 +27,18 @@ import org.thingsboard.server.common.data.id.TenantId; * @author Andrew Shvayka */ @Data +@ApiModel public class Event extends BaseData { + @ApiModelProperty(position = 1, value = "JSON object with Tenant Id.", readOnly = true) private TenantId tenantId; + @ApiModelProperty(position = 2, value = "Event type", example = "STATS") private String type; + @ApiModelProperty(position = 3, value = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") private String uid; + @ApiModelProperty(position = 4, value = "JSON object with Entity Id for which event is created.", readOnly = true) private EntityId entityId; + @ApiModelProperty(position = 5, value = "Event body.", dataType = "com.fasterxml.jackson.databind.JsonNode") private transient JsonNode body; public Event() { @@ -45,4 +53,9 @@ public class Event extends BaseData { super(event); } + @ApiModelProperty(position = 6, value = "Timestamp of the event creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java index f2985e3371..6202d7391f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java @@ -15,11 +15,17 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class HomeDashboard extends Dashboard { + public static final String HIDE_DASHBOARD_TOOLBAR_DESCRIPTION = "Hide dashboard toolbar flag. Useful for rendering dashboards on mobile."; + + @ApiModelProperty(position = 10, value = HIDE_DASHBOARD_TOOLBAR_DESCRIPTION) private boolean hideDashboardToolbar; public HomeDashboard(Dashboard dashboard, boolean hideDashboardToolbar) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java index 1cbc1c737e..dc6cdb55ef 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java @@ -15,13 +15,18 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.id.DashboardId; +@ApiModel @Data @AllArgsConstructor public class HomeDashboardInfo { + @ApiModelProperty(position = 1, value = "JSON object with the dashboard Id.") private DashboardId dashboardId; + @ApiModelProperty(position = 1, value = HomeDashboard.HIDE_DASHBOARD_TOOLBAR_DESCRIPTION) private boolean hideDashboardToolbar; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java index 3506aaea75..e60bb69ed9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackage.java @@ -15,18 +15,22 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.OtaPackageId; import java.nio.ByteBuffer; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) public class OtaPackage extends OtaPackageInfo { private static final long serialVersionUID = 3091601761339422546L; + @ApiModelProperty(position = 16, value = "OTA Package data.", readOnly = true) private transient ByteBuffer data; public OtaPackage() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index d50aa70dcb..25feeef682 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -16,15 +16,19 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; -import org.thingsboard.server.common.data.ota.OtaPackageType; 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.ota.ChecksumAlgorithm; +import org.thingsboard.server.common.data.ota.OtaPackageType; +@ApiModel @Slf4j @Data @EqualsAndHashCode(callSuper = true) @@ -32,21 +36,33 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo implements Has private static final long serialVersionUID = 7282664529021651736L; + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id. Tenant Id of the resource can't be changed.", readOnly = true) private TenantId tenantId; @NoXss + @ApiModelProperty(position = 4, value = "Resource title.", example = "BinaryAppDataContainer id=19 v1.0") private String title; + @ApiModelProperty(position = 5, value = "Resource type.", example = "LWM2M_MODEL", readOnly = true) private ResourceType resourceType; + @ApiModelProperty(position = 6, value = "Resource key.", example = "19_1.0", readOnly = true) private String resourceKey; + @ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", readOnly = true) private String searchText; public TbResourceInfo() { @@ -54,6 +62,21 @@ public class TbResourceInfo extends SearchTextBased implements Has this.searchText = resourceInfo.getSearchText(); } + @ApiModelProperty(position = 1, value = "JSON object with the Resource Id. " + + "Specify this field to update the Resource. " + + "Referencing non-existing Resource Id will cause error. " + + "Omit this field to create new Resource." ) + @Override + public TbResourceId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the resource creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + @Override @JsonIgnore public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index b6adf6cf65..f797591ccf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -17,20 +17,28 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.validation.NoXss; +@ApiModel @EqualsAndHashCode(callSuper = true) public class Tenant extends ContactBased implements HasTenantId { private static final long serialVersionUID = 8057243243859922101L; @NoXss + @ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A") private String title; @NoXss + @ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America") private String region; + + @ApiModelProperty(position = 6, required = true, value = "JSON object with Tenant Profile Id") private TenantProfileId tenantProfileId; public Tenant() { @@ -63,6 +71,7 @@ public class Tenant extends ContactBased implements HasTenantId { } @Override + @ApiModelProperty(position = 4, value = "Name of the tenant. Read-only, duplicated from title for backward compatibility", example = "Company A", readOnly = true) @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return title; @@ -89,6 +98,75 @@ public class Tenant extends ContactBased implements HasTenantId { return getTitle(); } + @ApiModelProperty(position = 1, value = "JSON object with the tenant Id. " + + "Specify this field to update the tenant. " + + "Referencing non-existing tenant Id will cause error. " + + "Omit this field to create new tenant." ) + @Override + public TenantId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the tenant creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 7, required = true, value = "Country", example = "US") + @Override + public String getCountry() { + return super.getCountry(); + } + + @ApiModelProperty(position = 8, required = true, value = "State", example = "NY") + @Override + public String getState() { + return super.getState(); + } + + @ApiModelProperty(position = 9, required = true, value = "City", example = "New York") + @Override + public String getCity() { + return super.getCity(); + } + + @ApiModelProperty(position = 10, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400") + @Override + public String getAddress() { + return super.getAddress(); + } + + @ApiModelProperty(position = 11, required = true, value = "Address Line 2", example = "") + @Override + public String getAddress2() { + return super.getAddress2(); + } + + @ApiModelProperty(position = 12, required = true, value = "Zip code", example = "10004") + @Override + public String getZip() { + return super.getZip(); + } + + @ApiModelProperty(position = 13, required = true, value = "Phone number", example = "+1(415)777-7777") + @Override + public String getPhone() { + return super.getPhone(); + } + + @ApiModelProperty(position = 14, required = true, value = "Email", example = "example@company.com") + @Override + public String getEmail() { + return super.getEmail(); + } + + @ApiModelProperty(position = 15, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java index acc83eca6f..4a98262f14 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantInfo.java @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; +@ApiModel @Data public class TenantInfo extends Tenant { - + @ApiModelProperty(position = 15, value = "Tenant Profile name", example = "Default") private String tenantProfileName; public TenantInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 05d22af09f..2d1a575f1c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.JsonProcessingException; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @@ -31,18 +33,27 @@ import java.util.Optional; import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) @Slf4j public class TenantProfile extends SearchTextBased implements HasName { @NoXss + @ApiModelProperty(position = 3, value = "Name of the tenant profile", example = "High Priority Tenants") private String name; @NoXss + @ApiModelProperty(position = 4, value = "Description of the tenant profile", example = "Any text") private String description; + @ApiModelProperty(position = 5, value = "Default Tenant profile to be used.", example = "true") private boolean isDefault; + @ApiModelProperty(position = 6, value = "If enabled, will push all messages related to this tenant and processed by core platform services into separate queue. " + + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") private boolean isolatedTbCore; + @ApiModelProperty(position = 7, value = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") private boolean isolatedTbRuleEngine; + @ApiModelProperty(position = 8, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; @JsonIgnore private byte[] profileDataBytes; @@ -65,6 +76,21 @@ public class TenantProfile extends SearchTextBased implements H this.setProfileData(tenantProfile.getProfileData()); } + @ApiModelProperty(position = 1, value = "JSON object with the tenant profile Id. " + + "Specify this field to update the tenant profile. " + + "Referencing non-existing tenant profile Id will cause error. " + + "Omit this field to create new tenant profile." ) + @Override + public TenantProfileId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the tenant profile creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + @Override public String getSearchText() { return getName(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java index dc88e6c295..78b08cd456 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UpdateMessage.java @@ -15,12 +15,17 @@ */ package org.thingsboard.server.common.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class UpdateMessage { + @ApiModelProperty(position = 1, value = "The message about new platform update available.") private final String message; + @ApiModelProperty(position = 1, value = "'True' if new platform update is available.") private final boolean isUpdateAvailable; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index 420aff71ce..f6c4c88eec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/User.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java @@ -17,6 +17,9 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -26,6 +29,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.validation.NoXss; +@ApiModel @EqualsAndHashCode(callSuper = true) public class User extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -58,6 +62,23 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.lastName = user.getLastName(); } + + @ApiModelProperty(position = 1, value = "JSON object with the User Id. " + + "Specify this field to update the device. " + + "Referencing non-existing User Id will cause error. " + + "Omit this field to create new customer." ) + @Override + public UserId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the user creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 3, value = "JSON object with the Tenant Id.", readOnly = true) public TenantId getTenantId() { return tenantId; } @@ -66,6 +87,7 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.tenantId = tenantId; } + @ApiModelProperty(position = 4, value = "JSON object with the Customer Id.", readOnly = true) public CustomerId getCustomerId() { return customerId; } @@ -74,6 +96,7 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.customerId = customerId; } + @ApiModelProperty(position = 5, required = true, value = "Email of the user", example = "user@example.com") public String getEmail() { return email; } @@ -82,12 +105,14 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.email = email; } + @ApiModelProperty(position = 6, readOnly = true, value = "Duplicates the email of the user, readonly", example = "user@example.com") @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { return email; } + @ApiModelProperty(position = 7, required = true, value = "Authority", example = "SYS_ADMIN, TENANT_ADMIN or CUSTOMER_USER") public Authority getAuthority() { return authority; } @@ -96,6 +121,7 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.authority = authority; } + @ApiModelProperty(position = 8, required = true, value = "First name of the user", example = "John") public String getFirstName() { return firstName; } @@ -104,6 +130,7 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.firstName = firstName; } + @ApiModelProperty(position = 9, required = true, value = "Last name of the user", example = "Doe") public String getLastName() { return lastName; } @@ -112,6 +139,12 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H this.lastName = lastName; } + @ApiModelProperty(position = 10, value = "Additional parameters of the user", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + @Override public String getSearchText() { return getEmail(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 54f7ab72f5..b59d62bda6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.alarm; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -34,23 +36,41 @@ import java.util.List; /** * Created by ashvayka on 11.05.17. */ +@ApiModel @Data @Builder @AllArgsConstructor public class Alarm extends BaseData implements HasName, HasTenantId, HasCustomerId { + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id", readOnly = true) private TenantId tenantId; + + @ApiModelProperty(position = 4, value = "JSON object with Customer Id", readOnly = true) private CustomerId customerId; + + @ApiModelProperty(position = 6, required = true, value = "representing type of the Alarm", example = "High Temperature Alarm") private String type; + @ApiModelProperty(position = 7, required = true, value = "JSON object with alarm originator id") private EntityId originator; + @ApiModelProperty(position = 8, required = true, value = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; + @ApiModelProperty(position = 9, required = true, value = "Alarm status", example = "CLEARED_UNACK") private AlarmStatus status; + @ApiModelProperty(position = 10, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; + @ApiModelProperty(position = 11, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; + @ApiModelProperty(position = 12, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") private long ackTs; + @ApiModelProperty(position = 13, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") private long clearTs; + @ApiModelProperty(position = 14, value = "JSON object with alarm details") private transient JsonNode details; + @ApiModelProperty(position = 15, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; + @ApiModelProperty(position = 16, value = "JSON array of relation types that should be used for propagation. " + + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") private List propagateRelationTypes; public Alarm() { @@ -81,7 +101,25 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @ApiModelProperty(position = 5, required = true, value = "representing type of the Alarm", example = "High Temperature Alarm") public String getName() { return type; } + + @ApiModelProperty(position = 1, value = "JSON object with the alarm Id. " + + "Specify this field to update the alarm. " + + "Referencing non-existing alarm Id will cause error. " + + "Omit this field to create new alarm." ) + @Override + public AlarmId getId() { + return super.getId(); + } + + + @ApiModelProperty(position = 2, value = "Timestamp of the alarm creation, in milliseconds", example = "1634058704567", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 80d2b21ecb..6919159424 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -15,10 +15,15 @@ */ package org.thingsboard.server.common.data.alarm; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +@ApiModel public class AlarmInfo extends Alarm { private static final long serialVersionUID = 2807343093519543363L; + @ApiModelProperty(position = 17, value = "Alarm originator name", example = "Thermostat") private String originatorName; public AlarmInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 594e6b635f..6caee2f111 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.asset; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; @@ -27,6 +30,7 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.util.Optional; +@ApiModel @EqualsAndHashCode(callSuper = true) public class Asset extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -67,6 +71,22 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements Optional.ofNullable(asset.getAdditionalInfo()).ifPresent(this::setAdditionalInfo); } + @ApiModelProperty(position = 1, value = "JSON object with the asset Id. " + + "Specify this field to update the asset. " + + "Referencing non-existing asset Id will cause error. " + + "Omit this field to create new asset.") + @Override + public AssetId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the asset creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true) public TenantId getTenantId() { return tenantId; } @@ -75,6 +95,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.tenantId = tenantId; } + @ApiModelProperty(position = 4, value = "JSON object with Customer Id. Use 'assignAssetToCustomer' to change the Customer Id.", readOnly = true) public CustomerId getCustomerId() { return customerId; } @@ -83,6 +104,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.customerId = customerId; } + @ApiModelProperty(position = 5, required = true, value = "Unique Asset Name in scope of Tenant", example = "Empire State Building") @Override public String getName() { return name; @@ -92,6 +114,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.name = name; } + @ApiModelProperty(position = 6, required = true, value = "Asset type", example = "Building") public String getType() { return type; } @@ -100,6 +123,7 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements this.type = type; } + @ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "NY Building") public String getLabel() { return label; } @@ -113,6 +137,12 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements return getName(); } + @ApiModelProperty(position = 8, value = "Additional parameters of the asset", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java index ad3de54fc4..59d451bac6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetInfo.java @@ -15,13 +15,18 @@ */ package org.thingsboard.server.common.data.asset; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.AssetId; +@ApiModel @Data public class AssetInfo extends Asset { + @ApiModelProperty(position = 9, value = "Title of the Customer that owns the asset.", readOnly = true) private String customerTitle; + @ApiModelProperty(position = 10, value = "Indicates special 'Public' Customer that is auto-generated to use the assets on public dashboards.", readOnly = true) private boolean customerIsPublic; public AssetInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java index fe916f0eaa..d3fcad8e93 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/AssetSearchQuery.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.asset; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -31,8 +32,11 @@ import java.util.List; @Data public class AssetSearchQuery { + @ApiModelProperty(position = 3, value = "Main search parameters.") private RelationsSearchParameters parameters; + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and asset (e.g. 'Contains' or 'Manages').") private String relationType; + @ApiModelProperty(position = 2, value = "Array of asset types to filter the related entities (e.g. 'Building', 'Vehicle').") private List assetTypes; public EntityRelationsQuery toEntitySearchQuery() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java index 1ce0d6af02..2e259d7fa4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java @@ -16,24 +16,37 @@ package org.thingsboard.server.common.data.audit; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.*; +@ApiModel @EqualsAndHashCode(callSuper = true) @Data public class AuditLog extends BaseData { + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id", readOnly = true) private TenantId tenantId; + @ApiModelProperty(position = 4, value = "JSON object with Customer Id", readOnly = true) private CustomerId customerId; + @ApiModelProperty(position = 5, value = "JSON object with Entity id", readOnly = true) private EntityId entityId; + @ApiModelProperty(position = 6, value = "Name of the logged entity", example = "Thermometer", readOnly = true) private String entityName; + @ApiModelProperty(position = 7, value = "JSON object with User id.", readOnly = true) private UserId userId; + @ApiModelProperty(position = 8, value = "Unique user name(email) of the user that performed some action on logged entity", example = "tenant@thingsboard.org", readOnly = true) private String userName; + @ApiModelProperty(position = 9, value = "String represented Action type", example = "ADDED", readOnly = true) private ActionType actionType; + @ApiModelProperty(position = 10, value = "JsonNode represented action data", readOnly = true) private JsonNode actionData; + @ApiModelProperty(position = 11, value = "String represented Action status", example = "SUCCESS", allowableValues = "SUCCESS,FAILURE", readOnly = true) private ActionStatus actionStatus; + @ApiModelProperty(position = 12, value = "Failure action details info. An empty string in case of action status type 'SUCCESS', otherwise includes stack trace of the caused exception.", readOnly = true) private String actionFailureDetails; public AuditLog() { @@ -57,4 +70,17 @@ public class AuditLog extends BaseData { this.actionStatus = auditLog.getActionStatus(); this.actionFailureDetails = auditLog.getActionFailureDetails(); } + + @ApiModelProperty(position = 2, value = "Timestamp of the auditLog creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 1, value = "JSON object with the auditLog Id") + @Override + public AuditLogId getId() { + return super.getId(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java index 9143fdfece..6449cf6729 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/DeviceSearchQuery.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -25,11 +27,15 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; import java.util.List; +@ApiModel @Data public class DeviceSearchQuery { + @ApiModelProperty(position = 3, value = "Main search parameters.") private RelationsSearchParameters parameters; + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and device (e.g. 'Contains' or 'Manages').") private String relationType; + @ApiModelProperty(position = 2, value = "Array of device types to filter the related entities (e.g. 'Temperature Sensor', 'Smoke Sensor').") private List deviceTypes; public EntityRelationsQuery toEntitySearchQuery() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java index ffae06336b..1ad613f105 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java @@ -15,9 +15,11 @@ */ package org.thingsboard.server.common.data.device.data; +import io.swagger.annotations.ApiModel; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; +@ApiModel @Data public class DefaultDeviceConfiguration implements DeviceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java index 3baadc599e..16f751a136 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java @@ -19,8 +19,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.annotations.ApiModel; import org.thingsboard.server.common.data.DeviceProfileType; +@ApiModel @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java index f212d4a9c8..26266b2b18 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java @@ -15,12 +15,17 @@ */ package org.thingsboard.server.common.data.device.data; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class DeviceData { + @ApiModelProperty(position = 1, value = "Device configuration for device profile type. DEFAULT is only supported value for now") private DeviceConfiguration configuration; + @ApiModelProperty(position = 2, value = "Device transport configuration used to connect the device") private DeviceTransportConfiguration transportConfiguration; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java index e5c2ce89d2..e39ed8ac15 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java @@ -19,10 +19,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.annotations.ApiModel; import org.thingsboard.server.common.data.DeviceTransportType; import java.io.Serializable; +@ApiModel @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index 90de622fb0..1898d201ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -16,18 +16,23 @@ package org.thingsboard.server.common.data.device.profile; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; import javax.validation.Valid; import java.util.List; +@ApiModel @Data @JsonIgnoreProperties(ignoreUnknown = true) public class AlarmCondition implements Serializable { @Valid + @ApiModelProperty(position = 1, value = "JSON array of alarm condition filters") private List condition; + @ApiModelProperty(position = 2, value = "JSON object representing alarm condition type") private AlarmConditionSpec spec; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java index 96fbd3465f..5a45fa6d68 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilter.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.KeyFilterPredicate; @@ -24,15 +26,20 @@ import javax.validation.Valid; import java.io.Serializable; +@ApiModel @Data public class AlarmConditionFilter implements Serializable { @Valid + @ApiModelProperty(position = 1, value = "JSON object for specifying alarm condition by specific key") private AlarmConditionFilterKey key; + @ApiModelProperty(position = 2, value = "String representation of the type of the value", example = "NUMERIC") private EntityKeyValueType valueType; @NoXss + @ApiModelProperty(position = 3, value = "Value used in Constant comparison. For other types, such as TIME_SERIES or ATTRIBUTE, the predicate condition is used") private Object value; @Valid + @ApiModelProperty(position = 4, value = "JSON object representing filter condition") private KeyFilterPredicate predicate; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java index 258e50a6a0..737db08c0b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmConditionFilterKey.java @@ -15,16 +15,21 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; +@ApiModel @Data public class AlarmConditionFilterKey implements Serializable { + @ApiModelProperty(position = 1, value = "The key type", example = "TIME_SERIES") private final AlarmConditionKeyType type; @NoXss + @ApiModelProperty(position = 2, value = "String value representing the key", example = "temp") private final String key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index f578cd15c3..8304a1fc6a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.validation.NoXss; @@ -23,15 +25,20 @@ import javax.validation.Valid; import java.io.Serializable; +@ApiModel @Data public class AlarmRule implements Serializable { @Valid + @ApiModelProperty(position = 1, value = "JSON object representing the alarm rule condition") private AlarmCondition condition; + @ApiModelProperty(position = 2, value = "JSON object representing time interval during which the rule is active") private AlarmSchedule schedule; // Advanced @NoXss + @ApiModelProperty(position = 3, value = "String value representing the additional details for an alarm rule") private String alarmDetails; + @ApiModelProperty(position = 4, value = "JSON object with the dashboard Id representing the reference to alarm details dashboard used by mobile application") private DashboardId dashboardId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index 7a99ccb41e..806925f244 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -15,28 +15,40 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.validation.NoXss; -import java.io.Serializable; import javax.validation.Valid; +import java.io.Serializable; import java.util.List; import java.util.TreeMap; +@ApiModel @Data public class DeviceProfileAlarm implements Serializable { + @ApiModelProperty(position = 1, value = "String value representing the alarm rule id", example = "highTemperatureAlarmID") private String id; @NoXss + @ApiModelProperty(position = 2, value = "String value representing type of the alarm", example = "High Temperature Alarm") private String alarmType; @Valid + @ApiModelProperty(position = 3, value = "Complex JSON object representing create alarm rules. The unique create alarm rule can be created for each alarm severity type. " + + "There can be 5 create alarm rules configured per a single alarm type. See method implementation notes and AlarmRule model for more details") private TreeMap createRules; @Valid + @ApiModelProperty(position = 4, value = "JSON object representing clear alarm rule") private AlarmRule clearRule; // Hidden in advanced settings + @ApiModelProperty(position = 5, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; + @ApiModelProperty(position = 6, value = "JSON array of relation types that should be used for propagation. " + + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") private List propagateRelationTypes; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java index ce019212eb..2d0a5fcf10 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileConfiguration.java @@ -29,7 +29,7 @@ import java.io.Serializable; include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = DefaultDeviceProfileConfiguration.class, name = "DEFAULT")}) + @JsonSubTypes.Type(value = DefaultDeviceProfileConfiguration.class, name = "DEFAULT")}) public interface DeviceProfileConfiguration extends Serializable { @JsonIgnore 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 f77e6388a5..d56edcd323 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 @@ -15,20 +15,27 @@ */ package org.thingsboard.server.common.data.device.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; -import java.io.Serializable; import javax.validation.Valid; +import java.io.Serializable; import java.util.List; +@ApiModel @Data public class DeviceProfileData implements Serializable { + @ApiModelProperty(position = 1, value = "JSON object of device profile configuration") private DeviceProfileConfiguration configuration; @Valid + @ApiModelProperty(position = 2, value = "JSON object of device profile transport configuration") private DeviceProfileTransportConfiguration transportConfiguration; + @ApiModelProperty(position = 3, value = "JSON object of provisioning strategy type per device profile") private DeviceProfileProvisionConfiguration provisionConfiguration; @Valid + @ApiModelProperty(position = 4, value = "JSON array of alarm rules configuration per device profile") private List alarms; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java index 2b0f1b8da1..f4582786d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileProvisionConfiguration.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import java.io.Serializable; - @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java index 2b1aba3c0f..93d2f28ab2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttTopics.java @@ -34,20 +34,25 @@ public class MqttTopics { private static final String SOFTWARE = "/sw"; private static final String CHUNK = "/chunk/"; private static final String ERROR = "/error"; - + private static final String TELEMETRY_SHORT = "/t"; + private static final String ATTRIBUTES_SHORT = "/a"; + private static final String RPC_SHORT = "/r"; + private static final String REQUEST_SHORT = "/req"; + private static final String RESPONSE_SHORT = "/res"; + private static final String JSON_SHORT = "j"; + private static final String PROTO_SHORT = "p"; private static final String ATTRIBUTES_RESPONSE = ATTRIBUTES + RESPONSE; private static final String ATTRIBUTES_REQUEST = ATTRIBUTES + REQUEST; - + private static final String ATTRIBUTES_RESPONSE_SHORT = ATTRIBUTES_SHORT + RESPONSE_SHORT + "/"; + private static final String ATTRIBUTES_REQUEST_SHORT = ATTRIBUTES_SHORT + REQUEST_SHORT + "/"; private static final String DEVICE_RPC_RESPONSE = RPC + RESPONSE + "/"; private static final String DEVICE_RPC_REQUEST = RPC + REQUEST + "/"; - + private static final String DEVICE_RPC_RESPONSE_SHORT = RPC_SHORT + RESPONSE_SHORT + "/"; + private static final String DEVICE_RPC_REQUEST_SHORT = RPC_SHORT + REQUEST_SHORT + "/"; private static final String DEVICE_ATTRIBUTES_RESPONSE = ATTRIBUTES_RESPONSE + "/"; private static final String DEVICE_ATTRIBUTES_REQUEST = ATTRIBUTES_REQUEST + "/"; - - // V1_JSON topics - + // v1 topics public static final String BASE_DEVICE_API_TOPIC = "v1/devices/me"; - public static final String DEVICE_RPC_RESPONSE_TOPIC = BASE_DEVICE_API_TOPIC + DEVICE_RPC_RESPONSE; public static final String DEVICE_RPC_RESPONSE_SUB_TOPIC = DEVICE_RPC_RESPONSE_TOPIC + SUB_TOPIC; public static final String DEVICE_RPC_REQUESTS_TOPIC = BASE_DEVICE_API_TOPIC + DEVICE_RPC_REQUEST; @@ -60,9 +65,7 @@ public class MqttTopics { public static final String DEVICE_ATTRIBUTES_TOPIC = BASE_DEVICE_API_TOPIC + ATTRIBUTES; public static final String DEVICE_PROVISION_REQUEST_TOPIC = PROVISION + REQUEST; public static final String DEVICE_PROVISION_RESPONSE_TOPIC = PROVISION + RESPONSE; - - // V1_JSON gateway topics - + // v1 gateway topics public static final String BASE_GATEWAY_API_TOPIC = "v1/gateway"; public static final String GATEWAY_CONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + CONNECT; public static final String GATEWAY_DISCONNECT_TOPIC = BASE_GATEWAY_API_TOPIC + DISCONNECT; @@ -72,22 +75,44 @@ public class MqttTopics { public static final String GATEWAY_RPC_TOPIC = BASE_GATEWAY_API_TOPIC + RPC; public static final String GATEWAY_ATTRIBUTES_REQUEST_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_REQUEST; public static final String GATEWAY_ATTRIBUTES_RESPONSE_TOPIC = BASE_GATEWAY_API_TOPIC + ATTRIBUTES_RESPONSE; - // v2 topics public static final String BASE_DEVICE_API_TOPIC_V2 = "v2"; - public static final String REQUEST_ID_PATTERN = "(?\\d+)"; public static final String CHUNK_PATTERN = "(?\\d+)"; - public static final String DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + REQUEST + "/" + REQUEST_ID_PATTERN + CHUNK + CHUNK_PATTERN; public static final String DEVICE_FIRMWARE_RESPONSES_TOPIC = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + RESPONSE + "/" + SUB_TOPIC + CHUNK + SUB_TOPIC; public static final String DEVICE_FIRMWARE_ERROR_TOPIC = BASE_DEVICE_API_TOPIC_V2 + FIRMWARE + ERROR; - public static final String DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT = BASE_DEVICE_API_TOPIC_V2 + "/%s" + RESPONSE + "/%s" + CHUNK + "%d"; - public static final String DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN = BASE_DEVICE_API_TOPIC_V2 + SOFTWARE + REQUEST + "/" + REQUEST_ID_PATTERN + CHUNK + CHUNK_PATTERN; public static final String DEVICE_SOFTWARE_RESPONSES_TOPIC = BASE_DEVICE_API_TOPIC_V2 + SOFTWARE + RESPONSE + "/" + SUB_TOPIC + CHUNK + SUB_TOPIC; public static final String DEVICE_SOFTWARE_ERROR_TOPIC = BASE_DEVICE_API_TOPIC_V2 + SOFTWARE + ERROR; + public static final String DEVICE_ATTRIBUTES_SHORT_TOPIC = BASE_DEVICE_API_TOPIC_V2 + ATTRIBUTES_SHORT; + public static final String DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC = BASE_DEVICE_API_TOPIC_V2 + ATTRIBUTES_SHORT + "/" + JSON_SHORT; + public static final String DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC = BASE_DEVICE_API_TOPIC_V2 + ATTRIBUTES_SHORT + "/" + PROTO_SHORT; + public static final String DEVICE_TELEMETRY_SHORT_TOPIC = BASE_DEVICE_API_TOPIC_V2 + TELEMETRY_SHORT; + public static final String DEVICE_TELEMETRY_SHORT_JSON_TOPIC = BASE_DEVICE_API_TOPIC_V2 + TELEMETRY_SHORT + "/" + JSON_SHORT; + public static final String DEVICE_TELEMETRY_SHORT_PROTO_TOPIC = BASE_DEVICE_API_TOPIC_V2 + TELEMETRY_SHORT + "/" + PROTO_SHORT; + public static final String DEVICE_RPC_RESPONSE_SHORT_TOPIC = BASE_DEVICE_API_TOPIC_V2 + DEVICE_RPC_RESPONSE_SHORT; + public static final String DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC = BASE_DEVICE_API_TOPIC_V2 + DEVICE_RPC_RESPONSE_SHORT + JSON_SHORT + "/"; + public static final String DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC = BASE_DEVICE_API_TOPIC_V2 + DEVICE_RPC_RESPONSE_SHORT + PROTO_SHORT + "/"; + public static final String DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC = DEVICE_RPC_RESPONSE_SHORT_TOPIC + SUB_TOPIC; + public static final String DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC = DEVICE_RPC_RESPONSE_SHORT_TOPIC + JSON_SHORT + "/" + SUB_TOPIC; + public static final String DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC = DEVICE_RPC_RESPONSE_SHORT_TOPIC + PROTO_SHORT + "/" + SUB_TOPIC; + public static final String DEVICE_RPC_REQUESTS_SHORT_TOPIC = BASE_DEVICE_API_TOPIC_V2 + DEVICE_RPC_REQUEST_SHORT; + public static final String DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC = BASE_DEVICE_API_TOPIC_V2 + DEVICE_RPC_REQUEST_SHORT + JSON_SHORT + "/"; + public static final String DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC = BASE_DEVICE_API_TOPIC_V2 + DEVICE_RPC_REQUEST_SHORT + PROTO_SHORT + "/"; + public static final String DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC = DEVICE_RPC_REQUESTS_SHORT_TOPIC + SUB_TOPIC; + public static final String DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC = DEVICE_RPC_REQUESTS_SHORT_TOPIC + JSON_SHORT + "/" + SUB_TOPIC; + public static final String DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC = DEVICE_RPC_REQUESTS_SHORT_TOPIC + PROTO_SHORT + "/" + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC_V2 + ATTRIBUTES_RESPONSE_SHORT; + public static final String DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_RESPONSE_SHORT_JSON_TOPIC_PREFIX = DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX + JSON_SHORT + "/"; + public static final String DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_SHORT_JSON_TOPIC_PREFIX + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_RESPONSE_SHORT_PROTO_TOPIC_PREFIX = DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX + PROTO_SHORT + "/"; + public static final String DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC = DEVICE_ATTRIBUTES_RESPONSE_SHORT_PROTO_TOPIC_PREFIX + SUB_TOPIC; + public static final String DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX = BASE_DEVICE_API_TOPIC_V2 + ATTRIBUTES_REQUEST_SHORT; + public static final String DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX = DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX + JSON_SHORT + "/"; + public static final String DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX = DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX + PROTO_SHORT + "/"; private MqttTopics() { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java index 348f7725b0..f08fe1c084 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.entityview; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -25,11 +27,15 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import java.util.Collections; import java.util.List; +@ApiModel @Data public class EntityViewSearchQuery { + @ApiModelProperty(position = 3, value = "Main search parameters.") private RelationsSearchParameters parameters; + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and device (e.g. 'Contains' or 'Manages').") private String relationType; + @ApiModelProperty(position = 2, value = "Array of entity view types to filter the related entities (e.g. 'Temperature Sensor', 'Smoke Sensor').") private List entityViewTypes; public EntityRelationsQuery toEntitySearchQuery() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java index a95949acbd..939e85d68c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugEvent.java @@ -15,22 +15,35 @@ */ package org.thingsboard.server.common.data.event; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @Data +@ApiModel public abstract class DebugEvent implements EventFilter { - private String msgDirectionType; - private String server; - private String dataSearch; - private String metadataSearch; - private String entityName; - private String relationType; - private String entityId; - private String msgType; - private boolean isError; - private String error; + @ApiModelProperty(position = 1, value = "String value representing msg direction type (incoming to entity or outcoming from entity)", allowableValues = "IN, OUT") + protected String msgDirectionType; + @ApiModelProperty(position = 2, value = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") + protected String server; + @ApiModelProperty(position = 3, value = "The case insensitive 'contains' filter based on data (key and value) for the message.", example = "humidity") + protected String dataSearch; + @ApiModelProperty(position = 4, value = "The case insensitive 'contains' filter based on metadata (key and value) for the message.", example = "deviceName") + protected String metadataSearch; + @ApiModelProperty(position = 5, value = "String value representing the entity type", allowableValues = "DEVICE") + protected String entityName; + @ApiModelProperty(position = 6, value = "String value representing the type of message routing", example = "Success") + protected String relationType; + @ApiModelProperty(position = 7, value = "String value representing the entity id in the event body (originator of the message)", example = "de9d54a0-2b7a-11ec-a3cc-23386423d98f") + protected String entityId; + @ApiModelProperty(position = 8, value = "String value representing the message type", example = "POST_TELEMETRY_REQUEST") + protected String msgType; + @ApiModelProperty(position = 9, value = "Boolean value to filter the errors", allowableValues = "false, true") + protected boolean isError; + @ApiModelProperty(position = 10, value = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") + protected String errorStr; public void setIsError(boolean isError) { this.isError = isError; @@ -39,7 +52,7 @@ public abstract class DebugEvent implements EventFilter { @Override public boolean hasFilterForJsonBody() { return !StringUtils.isEmpty(msgDirectionType) || !StringUtils.isEmpty(server) || !StringUtils.isEmpty(dataSearch) || !StringUtils.isEmpty(metadataSearch) - || !StringUtils.isEmpty(entityName) || !StringUtils.isEmpty(relationType) || !StringUtils.isEmpty(entityId) || !StringUtils.isEmpty(msgType) || !StringUtils.isEmpty(error) || isError; + || !StringUtils.isEmpty(entityName) || !StringUtils.isEmpty(relationType) || !StringUtils.isEmpty(entityId) || !StringUtils.isEmpty(msgType) || !StringUtils.isEmpty(errorStr) || isError; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java index cc16d780f0..60e28f3b6f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleChainEventFilter.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.event; +import io.swagger.annotations.ApiModel; + +@ApiModel public class DebugRuleChainEventFilter extends DebugEvent { @Override public EventType getEventType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java index abe73e6e85..42d04be1ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/DebugRuleNodeEventFilter.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.event; +import io.swagger.annotations.ApiModel; + +@ApiModel public class DebugRuleNodeEventFilter extends DebugEvent { @Override public EventType getEventType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java index c9e9886c64..aa86c0b9a2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/ErrorEventFilter.java @@ -15,14 +15,21 @@ */ package org.thingsboard.server.common.data.event; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @Data +@ApiModel public class ErrorEventFilter implements EventFilter { - private String server; - private String method; - private String error; + + @ApiModelProperty(position = 1, value = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") + protected String server; + @ApiModelProperty(position = 2, value = "String value representing the method name when the error happened", example = "onClusterEventMsg") + protected String method; + @ApiModelProperty(position = 3, value = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") + protected String errorStr; @Override public EventType getEventType() { @@ -31,6 +38,6 @@ public class ErrorEventFilter implements EventFilter { @Override public boolean hasFilterForJsonBody() { - return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(method) || !StringUtils.isEmpty(error); + return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(method) || !StringUtils.isEmpty(errorStr); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java index eebab7e7d4..872b8a0095 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/EventFilter.java @@ -15,11 +15,12 @@ */ package org.thingsboard.server.common.data.event; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +@ApiModel @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -32,7 +33,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = StatisticsEventFilter.class, name = "STATS") }) public interface EventFilter { - @JsonIgnore + + @ApiModelProperty(position = 1, required = true, value = "String value representing the event type", example = "STATS") EventType getEventType(); boolean hasFilterForJsonBody(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java index a871ccb106..f87ace2731 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/LifeCycleEventFilter.java @@ -15,15 +15,23 @@ */ package org.thingsboard.server.common.data.event; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @Data +@ApiModel public class LifeCycleEventFilter implements EventFilter { - private String server; - private String event; - private String status; - private String error; + + @ApiModelProperty(position = 1, value = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") + protected String server; + @ApiModelProperty(position = 2, value = "String value representing the lifecycle event type", example = "STARTED") + protected String event; + @ApiModelProperty(position = 3, value = "String value representing status of the lifecycle event", allowableValues = "Success, Failure") + protected String status; + @ApiModelProperty(position = 4, value = "The case insensitive 'contains' filter based on error message", example = "not present in the DB") + protected String errorStr; @Override public EventType getEventType() { @@ -32,6 +40,6 @@ public class LifeCycleEventFilter implements EventFilter { @Override public boolean hasFilterForJsonBody() { - return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(event) || !StringUtils.isEmpty(status) || !StringUtils.isEmpty(error); + return !StringUtils.isEmpty(server) || !StringUtils.isEmpty(event) || !StringUtils.isEmpty(status) || !StringUtils.isEmpty(errorStr); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java index 9fad8b4bca..4c16e0bde5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/event/StatisticsEventFilter.java @@ -15,14 +15,21 @@ */ package org.thingsboard.server.common.data.event; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.StringUtils; @Data +@ApiModel public class StatisticsEventFilter implements EventFilter { - private String server; - private Integer messagesProcessed; - private Integer errorsOccurred; + + @ApiModelProperty(position = 1, value = "String value representing the server name, identifier or ip address where the platform is running", example = "ip-172-31-24-152") + protected String server; + @ApiModelProperty(position = 2, value = "The minimum number of successfully processed messages", example = "25") + protected Integer messagesProcessed; + @ApiModelProperty(position = 3, value = "The minimum number of errors occurred during messages processing", example = "30") + protected Integer errorsOccurred; @Override public EventType getEventType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java index 251a8d5808..3211a88514 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AlarmId.java @@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@ApiModel public class AlarmId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +38,7 @@ public class AlarmId extends UUIDBased implements EntityId { return new AlarmId(UUID.fromString(alarmId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "ALARM", allowableValues = "ALARM") @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java index 83dda3069a..1ebcbf681e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ApiUsageStateId.java @@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@ApiModel public class ApiUsageStateId extends UUIDBased implements EntityId { @JsonCreator @@ -33,7 +36,7 @@ public class ApiUsageStateId extends UUIDBased implements EntityId { return new ApiUsageStateId(UUID.fromString(userId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "API_USAGE_STATE", allowableValues = "API_USAGE_STATE") @Override public EntityType getEntityType() { return EntityType.API_USAGE_STATE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java index 881e9c24a2..3361266905 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AssetId.java @@ -18,10 +18,13 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; +@ApiModel public class AssetId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,7 +38,7 @@ public class AssetId extends UUIDBased implements EntityId { return new AssetId(UUID.fromString(assetId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "ASSET", allowableValues = "ASSET") @Override public EntityType getEntityType() { return EntityType.ASSET; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java index 344449ecab..d40bf20655 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java @@ -20,8 +20,11 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; +@ApiModel public final class CustomerId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -31,7 +34,7 @@ public final class CustomerId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "CUSTOMER", allowableValues = "CUSTOMER") @Override public EntityType getEntityType() { return EntityType.CUSTOMER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java index 93ce73c3ca..ff8b7f8584 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java @@ -20,8 +20,11 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; +@ApiModel public class DashboardId extends UUIDBased implements EntityId { @JsonCreator @@ -33,7 +36,7 @@ public class DashboardId extends UUIDBased implements EntityId { return new DashboardId(UUID.fromString(dashboardId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "DASHBOARD", allowableValues = "DASHBOARD") @Override public EntityType getEntityType() { return EntityType.DASHBOARD; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java index 3aed66733c..f2df1471c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java @@ -20,8 +20,11 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; +@ApiModel public class DeviceId extends UUIDBased implements EntityId { private static final long serialVersionUID = 1L; @@ -35,8 +38,8 @@ public class DeviceId extends UUIDBased implements EntityId { return new DeviceId(UUID.fromString(deviceId)); } - @JsonIgnore @Override + @ApiModelProperty(position = 2, required = true, value = "string", example = "DEVICE", allowableValues = "DEVICE") public EntityType getEntityType() { return EntityType.DEVICE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java index e3c830409e..18e428257c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceProfileId.java @@ -20,6 +20,7 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; public class DeviceProfileId extends UUIDBased implements EntityId { @@ -35,7 +36,7 @@ public class DeviceProfileId extends UUIDBased implements EntityId { return new DeviceProfileId(UUID.fromString(deviceProfileId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "DEVICE_PROFILE", allowableValues = "DEVICE_PROFILE") @Override public EntityType getEntityType() { return EntityType.DEVICE_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java index 92421c39fe..0a1495a14c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EdgeId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -35,7 +36,7 @@ public class EdgeId extends UUIDBased implements EntityId { return new EdgeId(UUID.fromString(integrationId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "EDGE", allowableValues = "EDGE") @Override public EntityType getEntityType() { return EntityType.EDGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index 6057858fcd..73b4a5b28f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java @@ -18,6 +18,8 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.io.Serializable; @@ -29,12 +31,15 @@ import java.util.UUID; @JsonDeserialize(using = EntityIdDeserializer.class) @JsonSerialize(using = EntityIdSerializer.class) +@ApiModel public interface EntityId extends HasUUID, Serializable { //NOSONAR, the constant is closely related to EntityId UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080"); + @ApiModelProperty(position = 1, required = true, value = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") UUID getId(); + @ApiModelProperty(position = 2, required = true, value = "string", example = "DEVICE") EntityType getEntityType(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java index 26403228a3..599bd78473 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityViewId.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -37,6 +38,7 @@ public class EntityViewId extends UUIDBased implements EntityId { return new EntityViewId(UUID.fromString(entityViewID)); } + @ApiModelProperty(position = 2, required = true, value = "string", example = "ENTITY_VIEW", allowableValues = "ENTITY_VIEW") @Override public EntityType getEntityType() { return EntityType.ENTITY_VIEW; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java index 0442792238..3d439fa8ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/OtaPackageId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -35,7 +36,7 @@ public class OtaPackageId extends UUIDBased implements EntityId { return new OtaPackageId(UUID.fromString(firmwareId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "OTA_PACKAGE", allowableValues = "OTA_PACKAGE") @Override public EntityType getEntityType() { return EntityType.OTA_PACKAGE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java index a4cd8797b0..4e7782f6a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RpcId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -31,7 +32,7 @@ public final class RpcId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "RPC", allowableValues = "RPC") @Override public EntityType getEntityType() { return EntityType.RPC; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java index fc5aba9430..04a5a94608 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleChainId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -29,7 +30,7 @@ public class RuleChainId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "RULE_CHAIN", allowableValues = "RULE_CHAIN") @Override public EntityType getEntityType() { return EntityType.RULE_CHAIN; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java index 9b2da26d90..e448243dcb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleNodeId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -29,7 +30,7 @@ public class RuleNodeId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "RULE_NODE", allowableValues = "RULE_NODE") @Override public EntityType getEntityType() { return EntityType.RULE_NODE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java index 566b62db66..66b5d0f194 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TbResourceId.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.id; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; import java.util.UUID; @@ -31,7 +32,7 @@ public class TbResourceId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "TB_RESOURCE", allowableValues = "TB_RESOURCE") @Override public EntityType getEntityType() { return EntityType.TB_RESOURCE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java index b5c559b9b8..a785ac03d5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java @@ -20,6 +20,7 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; public final class TenantId extends UUIDBased implements EntityId { @@ -34,7 +35,7 @@ public final class TenantId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "TENANT", allowableValues = "TENANT") @Override public EntityType getEntityType() { return EntityType.TENANT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java index 70bce39a78..4c76aa707b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantProfileId.java @@ -20,6 +20,7 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; public class TenantProfileId extends UUIDBased implements EntityId { @@ -35,7 +36,7 @@ public class TenantProfileId extends UUIDBased implements EntityId { return new TenantProfileId(UUID.fromString(tenantProfileId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "TENANT_PROFILE", allowableValues = "TENANT_PROFILE") @Override public EntityType getEntityType() { return EntityType.TENANT_PROFILE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index a632e759e9..72a8636374 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -15,9 +15,13 @@ */ package org.thingsboard.server.common.data.id; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + import java.io.Serializable; import java.util.UUID; +@ApiModel public abstract class UUIDBased implements HasUUID, Serializable { private static final long serialVersionUID = 1L; @@ -33,6 +37,7 @@ public abstract class UUIDBased implements HasUUID, Serializable { this.id = id; } + @ApiModelProperty(position = 1, required = true, value = "string", example = "784f394c-42b6-435a-983c-b7beff2784f9") public UUID getId() { return id; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java index cab6a16242..aa4ca4da79 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java @@ -20,6 +20,7 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; public class UserId extends UUIDBased implements EntityId { @@ -33,7 +34,7 @@ public class UserId extends UUIDBased implements EntityId { return new UserId(UUID.fromString(userId)); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "USER", allowableValues = "USER") @Override public EntityType getEntityType() { return EntityType.USER; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java index 71591076d3..c75a99345f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java @@ -20,6 +20,7 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; public final class WidgetTypeId extends UUIDBased implements EntityId { @@ -31,7 +32,7 @@ public final class WidgetTypeId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "WIDGET_TYPE", allowableValues = "WIDGET_TYPE") @Override public EntityType getEntityType() { return EntityType.WIDGET_TYPE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java index 56ab09556c..2ad772a898 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java @@ -20,6 +20,7 @@ import java.util.UUID; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; import org.thingsboard.server.common.data.EntityType; public final class WidgetsBundleId extends UUIDBased implements EntityId { @@ -31,7 +32,7 @@ public final class WidgetsBundleId extends UUIDBased implements EntityId { super(id); } - @JsonIgnore + @ApiModelProperty(position = 2, required = true, value = "string", example = "WIDGETS_BUNDLE", allowableValues = "WIDGETS_BUNDLE") @Override public EntityType getEntityType() { return EntityType.WIDGETS_BUNDLE; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java index 1e0ff70e8a..7f9149e31e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mInstance.java @@ -15,11 +15,16 @@ */ package org.thingsboard.server.common.data.lwm2m; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class LwM2mInstance { + @ApiModelProperty(position = 1, value = "LwM2M Instance id.", example = "0") int id; + @ApiModelProperty(position = 2, value = "LwM2M Resource observe.") LwM2mResourceObserve[] resources; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java index 80174b430f..c7ed3c5a2c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mObject.java @@ -15,14 +15,23 @@ */ package org.thingsboard.server.common.data.lwm2m; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class LwM2mObject { + @ApiModelProperty(position = 1, value = "LwM2M Object id.", example = "19") int id; + @ApiModelProperty(position = 2, value = "LwM2M Object key id.", example = "19_1.0") String keyId; + @ApiModelProperty(position = 3, value = "LwM2M Object name.", example = "BinaryAppDataContainer") String name; + @ApiModelProperty(position = 4, value = "LwM2M Object multiple.", example = "true") boolean multiple; + @ApiModelProperty(position = 5, value = "LwM2M Object mandatory.", example = "false") boolean mandatory; + @ApiModelProperty(position = 6, value = "LwM2M Object instances.") LwM2mInstance [] instances; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java index 402309ebf3..86f5c16540 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/LwM2mResourceObserve.java @@ -15,19 +15,28 @@ */ package org.thingsboard.server.common.data.lwm2m; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import java.util.stream.Stream; +@ApiModel @Data @AllArgsConstructor public class LwM2mResourceObserve { + @ApiModelProperty(position = 1, value = "LwM2M Resource Observe id.", example = "0") int id; + @ApiModelProperty(position = 2, value = "LwM2M Resource Observe name.", example = "Data") String name; + @ApiModelProperty(position = 3, value = "LwM2M Resource Observe observe.", example = "false") boolean observe; + @ApiModelProperty(position = 4, value = "LwM2M Resource Observe attribute.", example = "false") boolean attribute; + @ApiModelProperty(position = 5, value = "LwM2M Resource Observe telemetry.", example = "false") boolean telemetry; + @ApiModelProperty(position = 6, value = "LwM2M Resource Observe key name.", example = "data") String keyName; public LwM2mResourceObserve(int id, String name, boolean observe, boolean attribute, boolean telemetry) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java index 8220702ec8..79b3ef40b6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java @@ -15,17 +15,30 @@ */ package org.thingsboard.server.common.data.lwm2m; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class ServerSecurityConfig { + @ApiModelProperty(position = 1, value = "Is Bootstrap Server", example = "true", readOnly = true) + boolean bootstrapServerIs = true; + @ApiModelProperty(position = 2, value = "Host for 'No Security' mode", example = "0.0.0.0", readOnly = true) String host; - String securityHost; + @ApiModelProperty(position = 3, value = "Port for 'No Security' mode", example = "5687", readOnly = true) Integer port; + @ApiModelProperty(position = 4, value = "Host for 'Security' mode (DTLS)", example = "0.0.0.0", readOnly = true) + String securityHost; + @ApiModelProperty(position = 5, value = "Port for 'Security' mode (DTLS)", example = "5688", readOnly = true) Integer securityPort; - String serverPublicKey; - boolean bootstrapServerIs = true; - Integer clientHoldOffTime = 1; + @ApiModelProperty(position = 5, value = "Server short Id", example = "111", readOnly = true) Integer serverId = 111; + @ApiModelProperty(position = 7, value = "Client Hold Off Time", example = "1", readOnly = true) + Integer clientHoldOffTime = 1; + @ApiModelProperty(position = 8, value = "Server Public Key (base64 encoded)", example = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAZ0pSaGKHk/GrDaUDnQZpeEdGwX7m3Ws+U/kiVat\n" + + "+44sgk3c8g0LotfMpLlZJPhPwJ6ipXV+O1r7IZUjBs3LNA==", readOnly = true) + String serverPublicKey; + @ApiModelProperty(position = 9, value = "Bootstrap Server Account Timeout", example = "0", readOnly = true) Integer bootstrapServerAccountTimeout = 0; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java index 9e90ce14eb..652929bc96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2BasicMapperConfig.java @@ -15,19 +15,36 @@ */ package org.thingsboard.server.common.data.oauth2; -import lombok.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; @Builder(toBuilder = true) @EqualsAndHashCode @Data @ToString +@ApiModel public class OAuth2BasicMapperConfig { + @ApiModelProperty(value = "Email attribute key of OAuth2 principal attributes. " + + "Must be specified for BASIC mapper type and cannot be specified for GITHUB type") private final String emailAttributeKey; + @ApiModelProperty(value = "First name attribute key") private final String firstNameAttributeKey; + @ApiModelProperty(value = "Last name attribute key") private final String lastNameAttributeKey; + @ApiModelProperty(value = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", required = true) private final TenantNameStrategyType tenantNameStrategy; + @ApiModelProperty(value = "Tenant name pattern for CUSTOM naming strategy. " + + "OAuth2 attributes in the pattern can be used by enclosing attribute key in '%{' and '}'", example = "%{email}") private final String tenantNamePattern; + @ApiModelProperty(value = "Customer name pattern. When creating a user on the first OAuth2 log in, if specified, " + + "customer name will be used to create or find existing customer in the platform and assign customerId to the user") private final String customerNamePattern; + @ApiModelProperty(value = "Name of the tenant's dashboard to set as default dashboard for newly created user") private final String defaultDashboardName; + @ApiModelProperty(value = "Whether default dashboard should be open in full screen") private final boolean alwaysFullScreen; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java index cf9478a33a..8e7d891eb9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientInfo.java @@ -15,19 +15,26 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; @EqualsAndHashCode @Data @NoArgsConstructor @AllArgsConstructor +@ApiModel public class OAuth2ClientInfo { + @ApiModelProperty(value = "OAuth2 client name", example = "GitHub") private String name; + @ApiModelProperty(value = "Name of the icon, displayed on OAuth2 log in button", example = "github-logo") private String icon; + @ApiModelProperty(value = "URI for OAuth2 log in. On HTTP GET request to this URI, it redirects to the OAuth2 provider page", + example = "/oauth2/authorization/8352f191-2b4d-11ec-9ed1-cbf57c026ecc") private String url; public OAuth2ClientInfo(OAuth2ClientInfo oauth2ClientInfo) { @@ -35,4 +42,5 @@ public class OAuth2ClientInfo { this.icon = oauth2ClientInfo.getIcon(); this.url = oauth2ClientInfo.getUrl(); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java index b46f23484f..56f866eaab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -29,20 +31,34 @@ import java.util.List; @Data @ToString @NoArgsConstructor +@ApiModel public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName { + @ApiModelProperty(value = "OAuth2 provider identifier (e.g. its name)", required = true) private String providerId; + @ApiModelProperty(value = "Default config for mapping OAuth2 log in response to platform entities") private OAuth2MapperConfig mapperConfig; + @ApiModelProperty(value = "Default authorization URI of the OAuth2 provider") private String authorizationUri; + @ApiModelProperty(value = "Default access token URI of the OAuth2 provider") private String accessTokenUri; + @ApiModelProperty(value = "Default OAuth scopes that will be requested from OAuth2 platform") private List scope; + @ApiModelProperty(value = "Default user info URI of the OAuth2 provider") private String userInfoUri; + @ApiModelProperty(value = "Default name of the username attribute in OAuth2 provider log in response") private String userNameAttributeName; + @ApiModelProperty(value = "Default JSON Web Key URI of the OAuth2 provider") private String jwkSetUri; + @ApiModelProperty(value = "Default client authentication method to use: 'BASIC' or 'POST'") private String clientAuthenticationMethod; + @ApiModelProperty(value = "Comment for OAuth2 provider") private String comment; + @ApiModelProperty(value = "Default log in button icon for OAuth2 provider") private String loginButtonIcon; + @ApiModelProperty(value = "Default OAuth2 provider label") private String loginButtonLabel; + @ApiModelProperty(value = "Help link for OAuth2 provider") private String helpLink; public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java index 9d9bd939da..d4182a3dba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2DomainInfo.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,7 +30,10 @@ import lombok.ToString; @NoArgsConstructor @AllArgsConstructor @Builder +@ApiModel public class OAuth2DomainInfo { + @ApiModelProperty(value = "Domain scheme. Mixed scheme means than both HTTP and HTTPS are going to be used", required = true) private SchemeType scheme; + @ApiModelProperty(value = "Domain name. Cannot be empty", required = true) private String name; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java index 72f4b06161..11c241e033 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Info.java @@ -15,7 +15,14 @@ */ package org.thingsboard.server.common.data.oauth2; -import lombok.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.List; @@ -25,7 +32,10 @@ import java.util.List; @Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor +@ApiModel public class OAuth2Info { + @ApiModelProperty("Whether OAuth2 settings are enabled or not") private boolean enabled; + @ApiModelProperty(value = "List of configured OAuth2 clients. Cannot contain null values", required = true) private List oauth2ParamsInfos; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java index 1c4454150f..df0cc45d7b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MapperConfig.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModelProperty; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; @@ -25,9 +26,14 @@ import lombok.ToString; @Data @ToString public class OAuth2MapperConfig { + @ApiModelProperty(value = "Whether user should be created if not yet present on the platform after successful authentication") private boolean allowUserCreation; + @ApiModelProperty(value = "Whether user credentials should be activated when user is created after successful authentication") private boolean activateUser; + @ApiModelProperty(value = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", required = true) private MapperType type; + @ApiModelProperty(value = "Mapper config for BASIC and GITHUB mapper types") private OAuth2BasicMapperConfig basic; + @ApiModelProperty(value = "Mapper config for CUSTOM mapper type") private OAuth2CustomMapperConfig custom; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java index c04a1a9fd1..5fe3bf5543 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -28,7 +30,10 @@ import lombok.ToString; @NoArgsConstructor @AllArgsConstructor @Builder +@ApiModel public class OAuth2MobileInfo { + @ApiModelProperty(value = "Application package name. Cannot be empty", required = true) private String pkgName; + @ApiModelProperty(value = "Application secret. The length must be at least 16 characters", required = true) private String appSecret; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java index 1a1d729d6b..f564291679 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ParamsInfo.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -30,10 +32,16 @@ import java.util.List; @Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor +@ApiModel public class OAuth2ParamsInfo { + @ApiModelProperty(value = "List of configured domains where OAuth2 platform will redirect a user after successful " + + "authentication. Cannot be empty. There have to be only one domain with specific name with scheme type 'MIXED'. " + + "Configured domains with the same name must have different scheme types", required = true) private List domainInfos; + @ApiModelProperty(value = "Mobile applications settings. Application package name must be unique within the list", required = true) private List mobileInfos; + @ApiModelProperty(value = "List of OAuth2 provider settings. Cannot be empty", required = true) private List clientRegistrations; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java index 3c9a1c1974..f40ca51d14 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2RegistrationInfo.java @@ -16,7 +16,14 @@ package org.thingsboard.server.common.data.oauth2; import com.fasterxml.jackson.databind.JsonNode; -import lombok.*; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; import java.util.List; @@ -26,19 +33,34 @@ import java.util.List; @NoArgsConstructor @AllArgsConstructor @Builder +@ApiModel public class OAuth2RegistrationInfo { + @ApiModelProperty(value = "Config for mapping OAuth2 log in response to platform entities", required = true) private OAuth2MapperConfig mapperConfig; + @ApiModelProperty(value = "OAuth2 client ID. Cannot be empty", required = true) private String clientId; + @ApiModelProperty(value = "OAuth2 client secret. Cannot be empty", required = true) private String clientSecret; + @ApiModelProperty(value = "Authorization URI of the OAuth2 provider. Cannot be empty", required = true) private String authorizationUri; + @ApiModelProperty(value = "Access token URI of the OAuth2 provider. Cannot be empty", required = true) private String accessTokenUri; + @ApiModelProperty(value = "OAuth scopes that will be requested from OAuth2 platform. Cannot be empty", required = true) private List scope; + @ApiModelProperty(value = "User info URI of the OAuth2 provider") private String userInfoUri; + @ApiModelProperty(value = "Name of the username attribute in OAuth2 provider response. Cannot be empty") private String userNameAttributeName; + @ApiModelProperty(value = "JSON Web Key URI of the OAuth2 provider") private String jwkSetUri; + @ApiModelProperty(value = "Client authentication method to use: 'BASIC' or 'POST'. Cannot be empty", required = true) private String clientAuthenticationMethod; + @ApiModelProperty(value = "OAuth2 provider label. Cannot be empty", required = true) private String loginButtonLabel; + @ApiModelProperty(value = "Log in button icon for OAuth2 provider") private String loginButtonIcon; + @ApiModelProperty(value = "List of platforms for which usage of the OAuth2 client is allowed (empty for all allowed)") private List platforms; + @ApiModelProperty(value = "Additional info of OAuth2 client (e.g. providerName)", required = true) private JsonNode additionalInfo; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java index e692aee468..e8cd73bca0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.objects; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.NoArgsConstructor; @@ -26,11 +28,15 @@ import java.util.List; * Created by Victor Basanets on 9/05/2017. */ @Data +@ApiModel @NoArgsConstructor public class AttributesEntityView implements Serializable { + @ApiModelProperty(position = 1, required = true, value = "List of client-side attribute keys to expose", example = "currentConfiguration") private List cs = new ArrayList<>(); + @ApiModelProperty(position = 3, required = true, value = "List of server-side attribute keys to expose", example = "model") private List ss = new ArrayList<>(); + @ApiModelProperty(position = 2, required = true, value = "List of shared attribute keys to expose", example = "targetConfiguration") private List sh = new ArrayList<>(); public AttributesEntityView(List cs, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java index 8819ee89bd..1cd3dc9cb4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.objects; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.NoArgsConstructor; @@ -25,11 +27,14 @@ import java.util.List; /** * Created by Victor Basanets on 9/05/2017. */ +@ApiModel @Data @NoArgsConstructor public class TelemetryEntityView implements Serializable { + @ApiModelProperty(position = 1, required = true, value = "List of time-series data keys to expose", example = "temperature, humidity") private List timeseries; + @ApiModelProperty(position = 2, required = true, value = "JSON object with attributes to expose") private AttributesEntityView attributes; public TelemetryEntityView(List timeseries, AttributesEntityView attributes) { 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 6ffbce4d3d..eb1d4ceca8 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 @@ -17,12 +17,15 @@ package org.thingsboard.server.common.data.page; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; +@ApiModel public class PageData { private final List data; @@ -45,18 +48,22 @@ public class PageData { this.hasNext = hasNext; } + @ApiModelProperty(position = 1, value = "Array of the entities", readOnly = true) public List getData() { return data; } + @ApiModelProperty(position = 2, value = "Total number of available pages. Calculated based on the 'pageSize' request parameter and total number of entities that match search criteria", readOnly = true) public int getTotalPages() { return totalPages; } + @ApiModelProperty(position = 3, value = "Total number of elements in all available pages", readOnly = true) public long getTotalElements() { return totalElements; } + @ApiModelProperty(position = 4, value = "'false' value indicates the end of the result set", readOnly = true) @JsonProperty("hasNext") public boolean hasNext() { return hasNext; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java index 1a6eb48eac..5c856cf38d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.plugin; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.*; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.ComponentDescriptorId; @@ -23,16 +25,23 @@ import org.thingsboard.server.common.data.id.ComponentDescriptorId; /** * @author Andrew Shvayka */ +@ApiModel @ToString public class ComponentDescriptor extends SearchTextBased { private static final long serialVersionUID = 1L; + @ApiModelProperty(position = 3, value = "Type of the Rule Node", readOnly = true) @Getter @Setter private ComponentType type; + @ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", readOnly = true, allowableValues = "TENANT", example = "TENANT") @Getter @Setter private ComponentScope scope; + @ApiModelProperty(position = 5, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", readOnly = true, example = "Custom Rule Node") @Getter @Setter private String name; + @ApiModelProperty(position = 6, value = "Full name of the Java class that implements the Rule Engine Node interface.", readOnly = true, example = "com.mycompany.CustomRuleNode") @Getter @Setter private String clazz; + @ApiModelProperty(position = 7, value = "Complex JSON object that represents the Rule Node configuration.", readOnly = true) @Getter @Setter private transient JsonNode configurationDescriptor; + @ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", readOnly = true) @Getter @Setter private String actions; public ComponentDescriptor() { @@ -53,12 +62,26 @@ public class ComponentDescriptor extends SearchTextBased this.actions = plugin.getActions(); } + @ApiModelProperty(position = 1, value = "JSON object with the descriptor Id. " + + "Specify existing descriptor id to update the descriptor. " + + "Referencing non-existing descriptor Id will cause error. " + + "Omit this field to create new descriptor." ) + @Override + public ComponentDescriptorId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the descriptor creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + @Override public String getSearchText() { return name; } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -84,4 +107,5 @@ public class ComponentDescriptor extends SearchTextBased result = 31 * result + (actions != null ? actions.hashCode() : 0); return result; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java index 07878a3347..a66a6c0954 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityCountQuery.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.annotations.ApiModel; import lombok.Getter; import java.util.Collections; import java.util.List; +@ApiModel public class EntityCountQuery { @Getter diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java index 233af353d1..2aba65c6a8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.annotations.ApiModel; import lombok.Data; import java.io.Serializable; +@ApiModel @Data public class EntityKey implements Serializable { private final EntityKeyType type; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java index 1b1ec51314..94884ef96a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/KeyFilter.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.common.data.query; +import io.swagger.annotations.ApiModel; import lombok.Data; import java.io.Serializable; +@ApiModel @Data public class KeyFilter implements Serializable { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 7833acc050..d3ff9ff5ca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -16,18 +16,17 @@ package org.thingsboard.server.common.data.relation; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.EntityId; -import java.io.ByteArrayInputStream; -import java.io.IOException; import java.io.Serializable; @Slf4j +@ApiModel public class EntityRelation implements Serializable { private static final long serialVersionUID = 2807343040519543363L; @@ -72,6 +71,7 @@ public class EntityRelation implements Serializable { this.additionalInfo = entityRelation.getAdditionalInfo(); } + @ApiModelProperty(position = 1, value = "JSON object with [from] Entity Id.", readOnly = true) public EntityId getFrom() { return from; } @@ -80,6 +80,7 @@ public class EntityRelation implements Serializable { this.from = from; } + @ApiModelProperty(position = 2, value = "JSON object with [to] Entity Id.", readOnly = true) public EntityId getTo() { return to; } @@ -88,6 +89,7 @@ public class EntityRelation implements Serializable { this.to = to; } + @ApiModelProperty(position = 3, value = "String value of relation type.", example = "Contains") public String getType() { return type; } @@ -96,6 +98,7 @@ public class EntityRelation implements Serializable { this.type = type; } + @ApiModelProperty(position = 4, value = "Represents the type group of the relation.", example = "COMMON") public RelationTypeGroup getTypeGroup() { return typeGroup; } @@ -104,6 +107,7 @@ public class EntityRelation implements Serializable { this.typeGroup = typeGroup; } + @ApiModelProperty(position = 5, value = "Additional parameters of the relation", dataType = "com.fasterxml.jackson.databind.JsonNode") public JsonNode getAdditionalInfo() { return SearchTextBasedWithAdditionalInfo.getJson(() -> additionalInfo, () -> additionalInfoBytes); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java index 614518d3f8..dc580312f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationInfo.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.relation; +import io.swagger.annotations.ApiModelProperty; + public class EntityRelationInfo extends EntityRelation { private static final long serialVersionUID = 2807343097519543363L; @@ -30,6 +32,7 @@ public class EntityRelationInfo extends EntityRelation { super(entityRelation); } + @ApiModelProperty(position = 6, value = "Name of the entity for [from] direction.", readOnly = true, example = "A4B72CCDFF33") public String getFromName() { return fromName; } @@ -38,6 +41,7 @@ public class EntityRelationInfo extends EntityRelation { this.fromName = fromName; } + @ApiModelProperty(position = 7, value = "Name of the entity for [to] direction.", readOnly = true, example = "A4B72CCDFF35") public String getToName() { return toName; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java index 1a5415d3c7..ef3b4eab23 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationsQuery.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.relation; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; @@ -23,9 +25,12 @@ import java.util.List; * Created by ashvayka on 02.05.17. */ @Data +@ApiModel public class EntityRelationsQuery { + @ApiModelProperty(position = 2, value = "Main search parameters.") private RelationsSearchParameters parameters; + @ApiModelProperty(position = 1, value = "Main filters.") private List filters; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java index 1e817dda14..95926a73f1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.relation; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.EntityType; @@ -26,9 +28,12 @@ import java.util.List; */ @Data @AllArgsConstructor +@ApiModel public class RelationEntityTypeFilter { + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains") private String relationType; + @ApiModelProperty(position = 2, value = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').") private List entityTypes; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java index f44337ff29..76e8415b5f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationsSearchParameters.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.relation; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.server.common.data.EntityType; @@ -26,15 +29,22 @@ import java.util.UUID; /** * Created by ashvayka on 03.05.17. */ +@ApiModel @Data @AllArgsConstructor public class RelationsSearchParameters { + @ApiModelProperty(position = 1, value = "Root entity id to start search from.", example = "784f394c-42b6-435a-983c-b7beff2784f9") private UUID rootId; + @ApiModelProperty(position = 2, value = "Type of the root entity.") private EntityType rootType; + @ApiModelProperty(position = 3, value = "Type of the root entity.") private EntitySearchDirection direction; + @ApiModelProperty(position = 4, value = "Type of the relation.") private RelationTypeGroup relationTypeGroup; + @ApiModelProperty(position = 5, value = "Maximum level of the search depth.") private int maxLevel = 1; + @ApiModelProperty(position = 6, value = "Fetch entities that match the last level of search. Useful to find Devices that are strictly 'maxLevel' relations away from the root entity.") private boolean fetchLastLevelOnly; public RelationsSearchParameters(EntityId entityId, EntitySearchDirection direction, int maxLevel, boolean fetchLastLevelOnly) { @@ -50,6 +60,7 @@ public class RelationsSearchParameters { this.fetchLastLevelOnly = fetchLastLevelOnly; } + @JsonIgnore public EntityId getEntityId() { return EntityIdFactory.getByTypeAndUuid(rootType, rootId); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rpc/Rpc.java b/common/data/src/main/java/org/thingsboard/server/common/data/rpc/Rpc.java index 27326087f7..cbe009d1b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rpc/Rpc.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rpc/Rpc.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.rpc; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; @@ -24,15 +26,24 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RpcId; import org.thingsboard.server.common.data.id.TenantId; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) public class Rpc extends BaseData implements HasTenantId { + + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true) private TenantId tenantId; + @ApiModelProperty(position = 4, value = "JSON object with Device Id.", readOnly = true) private DeviceId deviceId; + @ApiModelProperty(position = 5, value = "Expiration time of the request.", readOnly = true) private long expirationTime; + @ApiModelProperty(position = 6, value = "The request body that will be used to send message to device.", readOnly = true) private JsonNode request; + @ApiModelProperty(position = 7, value = "The response from the device.", readOnly = true) private JsonNode response; + @ApiModelProperty(position = 8, value = "The current status of the RPC call.", readOnly = true) private RpcStatus status; + @ApiModelProperty(position = 9, value = "Additional info used in the rule engine to process the updates to the RPC state.", readOnly = true) private JsonNode additionalInfo; public Rpc() { @@ -53,4 +64,17 @@ public class Rpc extends BaseData implements HasTenantId { this.status = rpc.getStatus(); this.additionalInfo = rpc.getAdditionalInfo(); } + + @ApiModelProperty(position = 1, value = "JSON object with the rpc Id. Referencing non-existing rpc Id will cause error.") + @Override + public RpcId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the rpc creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java index cbabe39938..e9a7bba35d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/DefaultRuleChainCreateRequest.java @@ -15,17 +15,21 @@ */ package org.thingsboard.server.common.data.rule; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.io.Serializable; +@ApiModel @Data @Slf4j public class DefaultRuleChainCreateRequest implements Serializable { private static final long serialVersionUID = 5600333716030561537L; + @ApiModelProperty(position = 1, required = true, value = "Name of the new rule chain", example = "Root Rule Chain") private String name; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java index 119af4ce4f..981fbb85dd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/NodeConnectionInfo.java @@ -15,14 +15,20 @@ */ package org.thingsboard.server.common.data.rule; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * Created by ashvayka on 21.03.18. */ +@ApiModel @Data public class NodeConnectionInfo { + @ApiModelProperty(position = 1, required = true, value = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") private int fromIndex; + @ApiModelProperty(position = 2, required = true, value = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'to' part of the connection.") private int toIndex; + @ApiModelProperty(position = 3, required = true, value = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") private String type; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java index da84f15427..6cfa13a867 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChain.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.rule; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @@ -28,6 +30,7 @@ import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.NoXss; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) @Slf4j @@ -35,13 +38,20 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im private static final long serialVersionUID = -5656679015121935465L; + @ApiModelProperty(position = 3, required = true, value = "JSON object with Tenant Id.", readOnly = true) private TenantId tenantId; @NoXss + @ApiModelProperty(position = 4, required = true, value = "Rule Chain name", example = "Humidity data processing") private String name; + @ApiModelProperty(position = 5, value = "Rule Chain type. 'EDGE' rule chains are processing messages on the edge devices only.", example = "A4B72CCDFF33") private RuleChainType type; + @ApiModelProperty(position = 6, value = "JSON object with Rule Chain Id. Pointer to the first rule node that should receive all messages pushed to this rule chain.") private RuleNodeId firstRuleNodeId; + @ApiModelProperty(position = 7, value = "Indicates root rule chain. The root rule chain process messages from all devices and entities by default. User may configure default rule chain per device profile.") private boolean root; + @ApiModelProperty(position = 8, value = "Reserved for future usage.") private boolean debugMode; + @ApiModelProperty(position = 9, value = "Reserved for future usage. The actual list of rule nodes and their relations is stored in the database separately.") private transient JsonNode configuration; @JsonIgnore @@ -75,6 +85,21 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im return name; } + @ApiModelProperty(position = 1, value = "JSON object with the Rule Chain Id. " + + "Specify this field to update the Rule Chain. " + + "Referencing non-existing Rule Chain Id will cause error. " + + "Omit this field to create new rule chain." ) + @Override + public RuleChainId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the rule chain creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + public JsonNode getConfiguration() { return SearchTextBasedWithAdditionalInfo.getJson(() -> configuration, () -> configurationBytes); } @@ -82,4 +107,5 @@ public class RuleChain extends SearchTextBasedWithAdditionalInfo im public void setConfiguration(JsonNode data) { setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java index 6b1646b8f1..68efc565be 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainConnectionInfo.java @@ -16,16 +16,23 @@ package org.thingsboard.server.common.data.rule; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.RuleChainId; /** * Created by ashvayka on 21.03.18. */ +@ApiModel @Data public class RuleChainConnectionInfo { + @ApiModelProperty(position = 1, required = true, value = "Index of rule node in the 'nodes' array of the RuleChainMetaData. Indicates the 'from' part of the connection.") private int fromIndex; + @ApiModelProperty(position = 2, required = true, value = "JSON object with the Rule Chain Id.") private RuleChainId targetRuleChainId; + @ApiModelProperty(position = 3, required = true, value = "JSON object with the additional information about the connection.") private JsonNode additionalInfo; + @ApiModelProperty(position = 4, required = true, value = "Type of the relation. Typically indicated the result of processing by the 'from' rule node. For example, 'Success' or 'Failure'") private String type; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java index 912120e0ad..8df7f24eba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainData.java @@ -15,13 +15,18 @@ */ package org.thingsboard.server.common.data.rule; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; +@ApiModel @Data public class RuleChainData { + @ApiModelProperty(position = 1, required = true, value = "List of the Rule Chain objects.", readOnly = true) List ruleChains; + @ApiModelProperty(position = 2, required = true, value = "List of the Rule Chain metadata objects.", readOnly = true) List metadata; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java index 11a2f68a57..70c30de947 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainImportResult.java @@ -15,17 +15,22 @@ */ package org.thingsboard.server.common.data.rule; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @Data -@AllArgsConstructor public class RuleChainImportResult { + @JsonIgnore private TenantId tenantId; private RuleChainId ruleChainId; - private ComponentLifecycleEvent lifecycleEvent; + private String ruleChainName; + @JsonInclude(JsonInclude.Include.NON_DEFAULT) + private boolean updated; + @JsonInclude(JsonInclude.Include.NON_NULL) + private String error; + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java index 33a4aaa6bb..0b069d7889 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java @@ -16,6 +16,8 @@ package org.thingsboard.server.common.data.rule; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.RuleChainId; @@ -25,17 +27,23 @@ import java.util.List; /** * Created by igor on 3/13/18. */ +@ApiModel @Data public class RuleChainMetaData { + @ApiModelProperty(position = 1, required = true, value = "JSON object with Rule Chain Id.", readOnly = true) private RuleChainId ruleChainId; + @ApiModelProperty(position = 2, required = true, value = "Index of the first rule node in the 'nodes' list") private Integer firstNodeIndex; + @ApiModelProperty(position = 3, required = true, value = "List of rule node JSON objects") private List nodes; + @ApiModelProperty(position = 4, required = true, value = "List of JSON objects that represent connections between rule nodes") private List connections; + @ApiModelProperty(position = 5, required = true, value = "List of JSON objects that represent connections between rule nodes and other rule chains.") private List ruleChainConnections; public void addConnectionInfo(int fromIndex, int toIndex, String type) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index 6d88ad6adc..a44595dcfa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.rule; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; @@ -25,6 +27,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) @Slf4j @@ -32,10 +35,15 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl private static final long serialVersionUID = -5656679015121235465L; + @ApiModelProperty(position = 3, value = "JSON object with the Rule Chain Id. ", readOnly = true) private RuleChainId ruleChainId; + @ApiModelProperty(position = 4, value = "Full Java Class Name of the rule node implementation. ", example = "com.mycompany.iot.rule.engine.ProcessingNode") private String type; + @ApiModelProperty(position = 5, value = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading") private String name; + @ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false") private boolean debugMode; + @ApiModelProperty(position = 7, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode") private transient JsonNode configuration; @JsonIgnore private byte[] configurationBytes; @@ -75,4 +83,25 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl setJson(data, json -> this.configuration = json, bytes -> this.configurationBytes = bytes); } + @ApiModelProperty(position = 1, value = "JSON object with the Rule Node Id. " + + "Specify this field to update the Rule Node. " + + "Referencing non-existing Rule Node Id will cause error. " + + "Omit this field to create new rule node." ) + @Override + public RuleNodeId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the rule node creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 8, value = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", dataType = "com.fasterxml.jackson.databind.JsonNode") + @Override + public JsonNode getAdditionalInfo() { + return super.getAdditionalInfo(); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java index 74adcdd06a..e7b448f800 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java @@ -15,16 +15,18 @@ */ package org.thingsboard.server.common.data.security; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; +@ApiModel @EqualsAndHashCode(callSuper = true) public class DeviceCredentials extends BaseData implements DeviceCredentialsFilter { private static final long serialVersionUID = -7869261127032877765L; - private DeviceId deviceId; private DeviceCredentialsType credentialsType; private String credentialsId; @@ -46,6 +48,21 @@ public class DeviceCredentials extends BaseData implements this.credentialsValue = deviceCredentials.getCredentialsValue(); } + @ApiModelProperty(position = 1, required = true, readOnly = true, value = "The Id is automatically generated during device creation. " + + "Use 'getDeviceCredentialsByDeviceId' to obtain the id based on device id. " + + "Use 'updateDeviceCredentials' to update device credentials. ", example = "784f394c-42b6-435a-983c-b7beff2784f9") + @Override + public DeviceCredentialsId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the device credentials creation, in milliseconds", example = "1609459200000") + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } + + @ApiModelProperty(position = 3, required = true, value = "JSON object with the device Id.") public DeviceId getDeviceId() { return deviceId; } @@ -54,6 +71,7 @@ public class DeviceCredentials extends BaseData implements this.deviceId = deviceId; } + @ApiModelProperty(position = 4, value = "Type of the credentials", allowableValues="ACCESS_TOKEN, X509_CERTIFICATE, MQTT_BASIC, LWM2M_CREDENTIALS") @Override public DeviceCredentialsType getCredentialsType() { return credentialsType; @@ -63,6 +81,11 @@ public class DeviceCredentials extends BaseData implements this.credentialsType = credentialsType; } + @ApiModelProperty(position = 5, required = true, value = "Unique Credentials Id per platform instance. " + + "Used to lookup credentials from the database. " + + "By default, new access token for your device. " + + "Depends on the type of the credentials." + , example = "Access token or other value that depends on the credentials type") @Override public String getCredentialsId() { return credentialsId; @@ -72,6 +95,9 @@ public class DeviceCredentials extends BaseData implements this.credentialsId = credentialsId; } + @ApiModelProperty(position = 6, value = "Value of the credentials. " + + "Null in case of ACCESS_TOKEN credentials type. Base64 value in case of X509_CERTIFICATE. " + + "Complex object in case of MQTT_BASIC and LWM2M_CREDENTIALS", example = "Null in case of ACCESS_TOKEN. See model definition.") public String getCredentialsValue() { return credentialsValue; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java index 5674df0ef1..5c1972d1e1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/SecuritySettings.java @@ -15,15 +15,20 @@ */ package org.thingsboard.server.common.data.security.model; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; +@ApiModel @Data public class SecuritySettings implements Serializable { + @ApiModelProperty(position = 1, value = "The user password policy object." ) private UserPasswordPolicy passwordPolicy; - + @ApiModelProperty(position = 2, value = "Maximum number of failed login attempts allowed before user account is locked." ) private Integer maxFailedLoginAttempts; + @ApiModelProperty(position = 3, value = "Email to use for notifications about locked users." ) private String userLockoutNotificationEmail; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java index f719316b37..28bdae82c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/UserPasswordPolicy.java @@ -15,20 +15,30 @@ */ package org.thingsboard.server.common.data.security.model; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; +@ApiModel @Data public class UserPasswordPolicy implements Serializable { + @ApiModelProperty(position = 1, value = "Minimum number of symbols in the password." ) private Integer minimumLength; + @ApiModelProperty(position = 1, value = "Minimum number of uppercase letters in the password." ) private Integer minimumUppercaseLetters; + @ApiModelProperty(position = 1, value = "Minimum number of lowercase letters in the password." ) private Integer minimumLowercaseLetters; + @ApiModelProperty(position = 1, value = "Minimum number of digits in the password." ) private Integer minimumDigits; + @ApiModelProperty(position = 1, value = "Minimum number of special in the password." ) private Integer minimumSpecialCharacters; + @ApiModelProperty(position = 1, value = "Password expiration period (days). Force expiration of the password." ) private Integer passwordExpirationPeriodDays; + @ApiModelProperty(position = 1, value = "Password reuse frequency (days). Disallow to use the same password for the defined number of days" ) private Integer passwordReuseFrequencyDays; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/AwsSnsSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/AwsSnsSmsProviderConfiguration.java index e5fd92b38d..08a3211c0a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/AwsSnsSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/AwsSnsSmsProviderConfiguration.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.sms.config; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class AwsSnsSmsProviderConfiguration implements SmsProviderConfiguration { + @ApiModelProperty(position = 1, value = "The AWS SNS Access Key ID.") private String accessKeyId; + @ApiModelProperty(position = 2, value = "The AWS SNS Access Key.") private String secretAccessKey; + @ApiModelProperty(position = 3, value = "The AWS region.") private String region; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TestSmsRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TestSmsRequest.java index 4e6d17ba9c..843d116cee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TestSmsRequest.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TestSmsRequest.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.sms.config; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class TestSmsRequest { + @ApiModelProperty(position = 1, value = "The SMS provider configuration") private SmsProviderConfiguration providerConfiguration; + @ApiModelProperty(position = 2, value = "The phone number or other identifier to specify as a recipient of the SMS.") private String numberTo; + @ApiModelProperty(position = 3, value = "The test message") private String message; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TwilioSmsProviderConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TwilioSmsProviderConfiguration.java index 800176472b..d6191fc9a5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TwilioSmsProviderConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sms/config/TwilioSmsProviderConfiguration.java @@ -15,13 +15,19 @@ */ package org.thingsboard.server.common.data.sms.config; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class TwilioSmsProviderConfiguration implements SmsProviderConfiguration { + @ApiModelProperty(position = 1, value = "Twilio account Sid.") private String accountSid; + @ApiModelProperty(position = 2, value = "Twilio account Token.") private String accountToken; + @ApiModelProperty(position = 3, value = "The number/id of a sender.") private String numberFrom; @Override 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 ce10f95055..7d4459c6d0 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 @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.tenant.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; 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 d88b92babc..6316305127 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 @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.swagger.annotations.ApiModel; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.TenantProfileType; 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 aaa22532bc..3138650504 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 @@ -15,11 +15,15 @@ */ package org.thingsboard.server.common.data.tenant.profile; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; +@ApiModel @Data public class TenantProfileData { + @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/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java index 300015fa23..0cc3e63b05 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.widget; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasTenantId; @@ -27,12 +28,16 @@ public class BaseWidgetType extends BaseData implements HasTenantI private static final long serialVersionUID = 8388684344603660756L; + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true) private TenantId tenantId; @NoXss + @ApiModelProperty(position = 4, value = "Reference to widget bundle", readOnly = true) private String bundleAlias; @NoXss + @ApiModelProperty(position = 5, value = "Unique alias that is used in dashboards as a reference widget type", readOnly = true) private String alias; @NoXss + @ApiModelProperty(position = 6, value = "Widget name used in search and UI", readOnly = true) private String name; public BaseWidgetType() { @@ -50,4 +55,19 @@ public class BaseWidgetType extends BaseData implements HasTenantI this.alias = widgetType.getAlias(); this.name = widgetType.getName(); } + + @ApiModelProperty(position = 1, value = "JSON object with the Widget Type Id. " + + "Specify this field to update the Widget Type. " + + "Referencing non-existing Widget Type Id will cause error. " + + "Omit this field to create new Widget Type." ) + @Override + public WidgetTypeId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the Widget Type creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); + } } 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 04432093ea..b2c5e5b3c2 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 @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.widget; import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; 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", readOnly = true) 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 00d7390ab7..fe7f87c7af 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 @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data.widget; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.validation.NoXss; @@ -24,8 +25,10 @@ import org.thingsboard.server.common.data.validation.NoXss; @JsonPropertyOrder({ "alias", "name", "image", "description", "descriptor" }) public class WidgetTypeDetails extends WidgetType { + @ApiModelProperty(position = 8, value = "Base64 encoded thumbnail", readOnly = true) private String image; @NoXss + @ApiModelProperty(position = 9, value = "Description of the widget", readOnly = true) private String description; public WidgetTypeDetails() { 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 2fde13552b..2898a05c13 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.widget; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.validation.NoXss; @@ -22,10 +23,13 @@ import org.thingsboard.server.common.data.validation.NoXss; @Data public class WidgetTypeInfo extends BaseWidgetType { + @ApiModelProperty(position = 7, value = "Base64 encoded widget thumbnail", readOnly = true) private String image; @NoXss + @ApiModelProperty(position = 7, value = "Description of the widget type", readOnly = true) private String description; @NoXss + @ApiModelProperty(position = 8, value = "Type of the widget (timeseries, latest, control, alarm or static)", readOnly = true) private String widgetType; public WidgetTypeInfo() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index 5ef6ff260a..17455d1e38 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -15,6 +15,10 @@ */ package org.thingsboard.server.common.data.widget; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Getter; +import lombok.Setter; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.TenantId; @@ -23,17 +27,37 @@ import org.thingsboard.server.common.data.validation.NoXss; import java.util.Arrays; +@ApiModel public class WidgetsBundle extends SearchTextBased implements HasTenantId { private static final long serialVersionUID = -7627368878362410489L; + @Getter + @Setter + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true) private TenantId tenantId; + @NoXss + @Getter + @Setter + @ApiModelProperty(position = 4, value = "Unique alias that is used in widget types as a reference widget bundle", readOnly = true) private String alias; + @NoXss + @Getter + @Setter + @ApiModelProperty(position = 5, value = "Title used in search and UI", readOnly = true) private String title; + + @Getter + @Setter + @ApiModelProperty(position = 6, value = "Base64 encoded thumbnail", readOnly = true) private String image; + @NoXss + @Getter + @Setter + @ApiModelProperty(position = 7, value = "Description", readOnly = true) private String description; public WidgetsBundle() { @@ -53,42 +77,21 @@ public class WidgetsBundle extends SearchTextBased implements H this.description = widgetsBundle.getDescription(); } - public TenantId getTenantId() { - return tenantId; - } - - public void setTenantId(TenantId tenantId) { - this.tenantId = tenantId; - } - - public String getAlias() { - return alias; - } - - public void setAlias(String alias) { - this.alias = alias; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getImage() { - return image; + @ApiModelProperty(position = 1, value = "JSON object with the Widget Bundle Id. " + + "Specify this field to update the Widget Bundle. " + + "Referencing non-existing Widget Bundle Id will cause error. " + + "Omit this field to create new Widget Bundle." ) + @Override + public WidgetsBundleId getId() { + return super.getId(); } - public void setImage(String image) { - this.image = image; + @ApiModelProperty(position = 2, value = "Timestamp of the Widget Bundle creation, in milliseconds", example = "1609459200000", readOnly = true) + @Override + public long getCreatedTime() { + return super.getCreatedTime(); } - public String getDescription() { return description; } - - public void setDescription(String description) { this.description = description; } - @Override public String getSearchText() { return getTitle(); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java new file mode 100644 index 0000000000..a53124860c --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/id/EntityIdTest.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2021 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.id; + +import org.junit.Assert; +import org.junit.Test; + +public class EntityIdTest { + + @Test + public void givenConstantNullUuid_whenCompare_thenToStringEqualsPredefinedUuid() { + Assert.assertEquals("13814000-1dd2-11b2-8080-808080808080", EntityId.NULL_UUID.toString()); + } + +} \ No newline at end of file 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 1207f4d634..83715994da 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 @@ -136,8 +136,9 @@ public final class TbMsg implements Serializable { tbMsg.data, ruleChainId, null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); } - public static TbMsg newMsg(TbMsg tbMsg, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(tbMsg.getQueueName(), UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), + //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(), tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ruleNodeExecCounter.get(), TbMsgCallback.EMPTY); } 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 a5aa451bd1..99e9c562a7 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 @@ -62,9 +62,6 @@ public class TbKafkaProducerTemplate implements TbQueuePro } this.settings = settings; - // Ugly workaround to fix org.apache.kafka.common.KafkaException: javax.security.auth.login.LoginException: unable to find LoginModule class - // details: https://stackoverflow.com/questions/57574901/kafka-java-client-classloader-doesnt-find-sasl-scram-login-class - Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); this.producer = new KafkaProducer<>(props); this.defaultTopic = defaultTopic; this.admin = admin; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index 1c8f84fbd0..1ad29524d2 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -20,7 +20,6 @@ import com.google.gson.JsonParser; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; @@ -32,6 +31,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.coap.CoapTransportResource; @@ -44,8 +44,9 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { @Override public TransportProtos.PostTelemetryMsg convertToPostTelemetry(UUID sessionId, Request inbound, Descriptors.Descriptor telemetryMsgDescriptor) throws AdaptorException { + ProtoConverter.validateDescriptor(telemetryMsgDescriptor); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); + return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), telemetryMsgDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -53,8 +54,9 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { @Override public TransportProtos.PostAttributeMsg convertToPostAttributes(UUID sessionId, Request inbound, Descriptors.Descriptor attributesMsgDescriptor) throws AdaptorException { + ProtoConverter.validateDescriptor(attributesMsgDescriptor); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); + return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), attributesMsgDescriptor))); } catch (Exception e) { throw new AdaptorException(e); } @@ -71,8 +73,9 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { if (requestId.isEmpty()) { throw new AdaptorException("Request id is missing!"); } else { + ProtoConverter.validateDescriptor(rpcResponseMsgDescriptor); try { - JsonElement response = new JsonParser().parse(dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); + JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(inbound.getPayload(), rpcResponseMsgDescriptor)); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId.orElseThrow(() -> new AdaptorException("Request id is missing!"))) .setPayload(response.toString()).build(); } catch (Exception e) { @@ -158,11 +161,6 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { return response; } - private String dynamicMsgToJson(byte[] bytes, Descriptors.Descriptor descriptor) throws InvalidProtocolBufferException { - DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, bytes); - return JsonFormat.printer().includingDefaultValueFields().print(dynamicMessage); - } - @Override public int getContentFormat() { return MediaTypeRegistry.APPLICATION_OCTET_STREAM; diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index dc1c49a3c0..a5045168af 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -17,6 +17,8 @@ package org.thingsboard.server.transport.http; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -68,6 +70,7 @@ import java.util.List; import java.util.UUID; import java.util.function.Consumer; + /** * @author Andrew Shvayka */ @@ -77,14 +80,70 @@ import java.util.function.Consumer; @Slf4j public class DeviceApiController implements TbTransportService { + private static final String MARKDOWN_CODE_BLOCK_START = "\n\n```json\n"; + private static final String MARKDOWN_CODE_BLOCK_END = "\n```\n\n"; + + private static final String REQUIRE_ACCESS_TOKEN = "The API call is designed to be used by device firmware and requires device access token ('deviceToken'). " + + "It is not recommended to use this API call by third-party scripts, rule-engine or platform widgets (use 'Telemetry Controller' instead).\n"; + + private static final String ATTRIBUTE_PAYLOAD_EXAMPLE = "{\n" + + " \"stringKey\":\"value1\", \n" + + " \"booleanKey\":true, \n" + + " \"doubleKey\":42.0, \n" + + " \"longKey\":73, \n" + + " \"jsonKey\": {\n" + + " \"someNumber\": 42,\n" + + " \"someArray\": [1,2,3],\n" + + " \"someNestedObject\": {\"key\": \"value\"}\n" + + " }\n" + + "}"; + + protected static final String TS_PAYLOAD = "The request payload is a JSON document with three possible formats:\n\n" + + "Simple format without timestamp. In such a case, current server time will be used: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"stringKey\":\"value1\", \n" + + " \"booleanKey\":true, \n" + + " \"doubleKey\":42.0, \n" + + " \"longKey\":73, \n" + + " \"jsonKey\": {\n" + + " \"someNumber\": 42,\n" + + " \"someArray\": [1,2,3],\n" + + " \"someNestedObject\": {\"key\": \"value\"}\n" + + " }\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n Single JSON object with timestamp: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}" + + MARKDOWN_CODE_BLOCK_END + + "\n\n JSON array with timestamps: \n\n" + + MARKDOWN_CODE_BLOCK_START + + "[\n{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, \n{\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}\n]" + + MARKDOWN_CODE_BLOCK_END; + + private static final String ACCESS_TOKEN_PARAM_DESCRIPTION = "Your device access token."; + @Autowired private HttpTransportContext transportContext; + @ApiOperation(value = "Get attributes (getDeviceAttributes)", + notes = "Returns all attributes that belong to device. " + + "Use optional 'clientKeys' and/or 'sharedKeys' parameter to return specific attributes. " + + "\n Example of the result: " + + MARKDOWN_CODE_BLOCK_START + + ATTRIBUTE_PAYLOAD_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json") - public DeferredResult getDeviceAttributes(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys, - @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys, - HttpServletRequest httpRequest) { + public DeferredResult getDeviceAttributes( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "Comma separated key names for attribute with client scope", required = true, defaultValue = "state") + @RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys, + @ApiParam(value = "Comma separated key names for attribute with shared scope", required = true, defaultValue = "configuration") + @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -106,9 +165,20 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Post attributes (postDeviceAttributes)", + notes = "Post client attribute updates on behalf of device. " + + "\n Example of the request: " + + MARKDOWN_CODE_BLOCK_START + + ATTRIBUTE_PAYLOAD_EXAMPLE + + MARKDOWN_CODE_BLOCK_END + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.POST) - public DeferredResult postDeviceAttributes(@PathVariable("deviceToken") String deviceToken, - @RequestBody String json, HttpServletRequest request) { + public DeferredResult postDeviceAttributes( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "JSON with attribute key-value pairs. See API call description for example.") + @RequestBody String json) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -119,9 +189,17 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Post time-series data (postTelemetry)", + notes = "Post time-series data on behalf of device. " + + "\n Example of the request: " + + TS_PAYLOAD + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/telemetry", method = RequestMethod.POST) - public DeferredResult postTelemetry(@PathVariable("deviceToken") String deviceToken, - @RequestBody String json, HttpServletRequest request) { + public DeferredResult postTelemetry( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -132,9 +210,22 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Save claiming information (claimDevice)", + notes = "Saves the information required for user to claim the device. " + + "See more info about claiming in the corresponding 'Claiming devices' platform documentation." + + "\n Example of the request payload: " + + MARKDOWN_CODE_BLOCK_START + + "{\"secretKey\":\"value\", \"durationMs\":60000}" + + MARKDOWN_CODE_BLOCK_END + + "Note: both 'secretKey' and 'durationMs' is optional parameters. " + + "In case the secretKey is not specified, the empty string as a default value is used. In case the durationMs is not specified, the system parameter device.claim.duration is used.\n\n" + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/claim", method = RequestMethod.POST) - public DeferredResult claimDevice(@PathVariable("deviceToken") String deviceToken, - @RequestBody(required = false) String json, HttpServletRequest request) { + public DeferredResult claimDevice( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @RequestBody(required = false) String json) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -146,10 +237,18 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Subscribe to RPC commands (subscribeToCommands) (Deprecated)", + notes = "Subscribes to RPC commands using http long polling. " + + "Deprecated, since long polling is resource and network consuming. " + + "Consider using MQTT or CoAP protocol for light-weight real-time updates. \n\n" + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.GET, produces = "application/json") - public DeferredResult subscribeToCommands(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, - HttpServletRequest httpRequest) { + public DeferredResult subscribeToCommands( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "Optional timeout of the long poll. Typically less then 60 seconds, since limited on the server side.") + @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -164,10 +263,18 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Reply to RPC commands (replyToCommand)", + notes = "Replies to server originated RPC command identified by 'requestId' parameter. The response is arbitrary JSON.\n\n" + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/rpc/{requestId}", method = RequestMethod.POST) - public DeferredResult replyToCommand(@PathVariable("deviceToken") String deviceToken, - @PathVariable("requestId") Integer requestId, - @RequestBody String json, HttpServletRequest request) { + public DeferredResult replyToCommand( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "RPC request id from the incoming RPC request", required = true, defaultValue = "123") + @PathVariable("requestId") Integer requestId, + @ApiParam(value = "Reply to the RPC request, JSON. For example: {\"status\":\"success\"}", required = true) + @RequestBody String json) { DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -177,9 +284,23 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Send the RPC command (postRpcRequest)", + notes = "Send the RPC request to server. The request payload is a JSON document that contains 'method' and 'params'. For example:" + + MARKDOWN_CODE_BLOCK_START + + "{\"method\": \"sumOnServer\", \"params\":{\"a\":2, \"b\":2}}" + + MARKDOWN_CODE_BLOCK_END + + "The response contains arbitrary JSON with the RPC reply. For example: " + + MARKDOWN_CODE_BLOCK_START + + "{\"result\": 4}" + + MARKDOWN_CODE_BLOCK_END + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.POST) - public DeferredResult postRpcRequest(@PathVariable("deviceToken") String deviceToken, - @RequestBody String json, HttpServletRequest httpRequest) { + public DeferredResult postRpcRequest( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "The RPC request JSON", required = true) + @RequestBody String json) { DeferredResult responseWriter = new DeferredResult(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -196,10 +317,18 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Subscribe to attribute updates (subscribeToAttributes) (Deprecated)", + notes = "Subscribes to client and shared scope attribute updates using http long polling. " + + "Deprecated, since long polling is resource and network consuming. " + + "Consider using MQTT or CoAP protocol for light-weight real-time updates. \n\n" + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/attributes/updates", method = RequestMethod.GET, produces = "application/json") - public DeferredResult subscribeToAttributes(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, - HttpServletRequest httpRequest) { + public DeferredResult subscribeToAttributes( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "Optional timeout of the long poll. Typically less then 60 seconds, since limited on the server side.") + @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(), new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> { @@ -214,26 +343,85 @@ public class DeviceApiController implements TbTransportService { return responseWriter; } + @ApiOperation(value = "Get Device Firmware (getFirmware)", + notes = "Downloads the current firmware package." + + "When the platform initiates firmware update, " + + "it informs the device by updating the 'fw_title', 'fw_version', 'fw_checksum' and 'fw_checksum_algorithm' shared attributes." + + "The 'fw_title' and 'fw_version' parameters must be supplied in this request to double-check " + + "that the firmware that device is downloading matches the firmware it expects to download. " + + "This is important, since the administrator may change the firmware assignment while device is downloading the firmware. \n\n" + + "Optional 'chunk' and 'size' parameters may be used to download the firmware in chunks. " + + "For example, device may request first 16 KB of firmware using 'chunk'=0 and 'size'=16384. " + + "Next 16KB using 'chunk'=1 and 'size'=16384. The last chunk should have less bytes then requested using 'size' parameter. \n\n" + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/firmware", method = RequestMethod.GET) - public DeferredResult getFirmware(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "title") String title, - @RequestParam(value = "version") String version, - @RequestParam(value = "size", required = false, defaultValue = "0") int size, - @RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) { + public DeferredResult getFirmware( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "Title of the firmware, corresponds to the value of 'fw_title' attribute.", required = true) + @RequestParam(value = "title") String title, + @ApiParam(value = "Version of the firmware, corresponds to the value of 'fw_version' attribute.", required = true) + @RequestParam(value = "version") String version, + @ApiParam(value = "Size of the chunk. Optional. Omit to download the entire file without chunks.") + @RequestParam(value = "size", required = false, defaultValue = "0") int size, + @ApiParam(value = "Index of the chunk. Optional. Omit to download the entire file without chunks.") + @RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) { return getOtaPackageCallback(deviceToken, title, version, size, chunk, OtaPackageType.FIRMWARE); } + @ApiOperation(value = "Get Device Software (getSoftware)", + notes = "Downloads the current software package." + + "When the platform initiates software update, " + + "it informs the device by updating the 'sw_title', 'sw_version', 'sw_checksum' and 'sw_checksum_algorithm' shared attributes." + + "The 'sw_title' and 'sw_version' parameters must be supplied in this request to double-check " + + "that the software that device is downloading matches the software it expects to download. " + + "This is important, since the administrator may change the software assignment while device is downloading the software. \n\n" + + "Optional 'chunk' and 'size' parameters may be used to download the software in chunks. " + + "For example, device may request first 16 KB of software using 'chunk'=0 and 'size'=16384. " + + "Next 16KB using 'chunk'=1 and 'size'=16384. The last chunk should have less bytes then requested using 'size' parameter. \n\n" + + REQUIRE_ACCESS_TOKEN, + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/{deviceToken}/software", method = RequestMethod.GET) - public DeferredResult getSoftware(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "title") String title, - @RequestParam(value = "version") String version, - @RequestParam(value = "size", required = false, defaultValue = "0") int size, - @RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) { + public DeferredResult getSoftware( + @ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN") + @PathVariable("deviceToken") String deviceToken, + @ApiParam(value = "Title of the software, corresponds to the value of 'sw_title' attribute.", required = true) + @RequestParam(value = "title") String title, + @ApiParam(value = "Version of the software, corresponds to the value of 'sw_version' attribute.", required = true) + @RequestParam(value = "version") String version, + @ApiParam(value = "Size of the chunk. Optional. Omit to download the entire file without using chunks.") + @RequestParam(value = "size", required = false, defaultValue = "0") int size, + @ApiParam(value = "Index of the chunk. Optional. Omit to download the entire file without using chunks.") + @RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) { return getOtaPackageCallback(deviceToken, title, version, size, chunk, OtaPackageType.SOFTWARE); } + @ApiOperation(value = "Provision new device (provisionDevice)", + notes = "Exchange the provision request to the device credentials. " + + "See more info about provisioning in the corresponding 'Device provisioning' platform documentation." + + "Requires valid JSON request with the following format: " + + MARKDOWN_CODE_BLOCK_START + + "{\n" + + " \"deviceName\": \"NEW_DEVICE_NAME\",\n" + + " \"provisionDeviceKey\": \"u7piawkboq8v32dmcmpp\",\n" + + " \"provisionDeviceSecret\": \"jpmwdn8ptlswmf4m29bw\"\n" + + "}" + + MARKDOWN_CODE_BLOCK_END + + "Where 'deviceName' is the name of enw or existing device which depends on the provisioning strategy. " + + "The 'provisionDeviceKey' and 'provisionDeviceSecret' matches info configured in one of the existing device profiles. " + + "The result of the successful call is the JSON object that contains new credentials:" + + MARKDOWN_CODE_BLOCK_START + "{\n" + + " \"credentialsType\":\"ACCESS_TOKEN\",\n" + + " \"credentialsValue\":\"DEVICE_ACCESS_TOKEN\",\n" + + " \"status\":\"SUCCESS\"\n" + + "}" + MARKDOWN_CODE_BLOCK_END + , + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/provision", method = RequestMethod.POST) - public DeferredResult provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) { + public DeferredResult provisionDevice( + @ApiParam(value = "JSON with provision request. See API call description for example.") + @RequestBody String json) { DeferredResult responseWriter = new DeferredResult<>(); transportContext.getTransportService().process(JsonConverter.convertToProvisionRequestMsg(json), new DeviceProvisionCallback(responseWriter)); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java index 6cd1ce05b2..4231d85f9e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java @@ -33,10 +33,6 @@ import java.security.KeyStore; @ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { - @Getter - @Setter - private LwM2mModelProvider modelProvider; - @Getter @Value("${transport.lwm2m.timeout:}") private Long timeout; @@ -140,6 +136,4 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { } } - - } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index 0a8f663dd8..c4b9ea3f27 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -62,14 +62,13 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; - private final LwM2mTransportServerHelper helper; private final OtaPackageDataCache otaPackageDataCache; private final DefaultLwM2mUplinkMsgHandler handler; private final CaliforniumRegistrationStore registrationStore; private final TbSecurityStore securityStore; - private final LwM2mClientContext lwM2mClientContext; private final TbLwM2MDtlsCertificateVerifier certificateVerifier; private final TbLwM2MAuthorizer authorizer; + private final LwM2mVersionedModelProvider modelProvider; private LeshanServer server; @@ -118,8 +117,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { builder.setCoapConfig(getCoapConfig(config.getPort(), config.getSecurePort(), config)); /* Define model provider (Create Models )*/ - LwM2mModelProvider modelProvider = new LwM2mVersionedModelProvider(this.lwM2mClientContext, this.helper, this.context); - config.setModelProvider(modelProvider); builder.setObjectModelProvider(modelProvider); /* Set securityStore with new registrationStore */ diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java index 463823e583..b3c07c91b9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mSessionMsgListener.java @@ -96,15 +96,15 @@ public class LwM2mSessionMsgListener implements GenericFutureListener resourceUpdateMsgOpt) { - if (ResourceType.LWM2M_MODEL.name().equals(resourceUpdateMsgOpt.get().getResourceType())) { + public void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt) { + if (ResourceType.LWM2M_MODEL.name().equals(resourceUpdateMsgOpt.getResourceType())) { this.handler.onResourceUpdate(resourceUpdateMsgOpt); } } @Override - public void onResourceDelete(@NotNull Optional resourceDeleteMsgOpt) { - if (ResourceType.LWM2M_MODEL.name().equals(resourceDeleteMsgOpt.get().getResourceType())) { + public void onResourceDelete(TransportProtos.ResourceDeleteMsg resourceDeleteMsgOpt) { + if (ResourceType.LWM2M_MODEL.name().equals(resourceDeleteMsgOpt.getResourceType())) { this.handler.onResourceDelete(resourceDeleteMsgOpt); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java index 650726604f..9448d9c0a4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.transport.lwm2m.server; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.eclipse.leshan.core.model.LwM2mModel; @@ -23,8 +22,11 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.eclipse.leshan.server.registration.Registration; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import java.util.ArrayList; @@ -32,47 +34,58 @@ import java.util.Base64; import java.util.Collection; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import static org.thingsboard.server.common.data.ResourceType.LWM2M_MODEL; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_KEY; @Slf4j -@RequiredArgsConstructor +@Service +@TbLwM2mTransportComponent public class LwM2mVersionedModelProvider implements LwM2mModelProvider { - /** - * int objectId - * String version ("1.01") - * Key = objectId + "##" + version - * Value = TenantId - */ private final LwM2mClientContext lwM2mClientContext; private final LwM2mTransportServerHelper helper; private final LwM2mTransportContext context; + private final ConcurrentMap> models; + + public LwM2mVersionedModelProvider(@Lazy LwM2mClientContext lwM2mClientContext, LwM2mTransportServerHelper helper, LwM2mTransportContext context) { + this.lwM2mClientContext = lwM2mClientContext; + this.helper = helper; + this.context = context; + this.models = new ConcurrentHashMap<>(); + } private String getKeyIdVer(Integer objectId, String version) { return objectId != null ? objectId + LWM2M_SEPARATOR_KEY + ((version == null || version.isEmpty()) ? ObjectModel.DEFAULT_VERSION : version) : null; } - /** - * Update repository if need - * - * @param registration - * @return - */ @Override public LwM2mModel getObjectModel(Registration registration) { return new DynamicModel(registration); } - private class DynamicModel implements LwM2mModel { + public void evict(TenantId tenantId, String key) { + if (tenantId.isNullUid()) { + models.values().forEach(m -> m.remove(key)); + } else { + models.get(tenantId).remove(key); + } + } + private class DynamicModel implements LwM2mModel { private final Registration registration; private final TenantId tenantId; + private final Lock modelsLock; public DynamicModel(Registration registration) { this.registration = registration; this.tenantId = lwM2mClientContext.getClientByEndpoint(registration.getEndpoint()).getTenantId(); + this.modelsLock = new ReentrantLock(); + models.computeIfAbsent(tenantId, t -> new ConcurrentHashMap<>()); } @Override @@ -114,6 +127,25 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider { private ObjectModel getObjectModelDynamic(Integer objectId, String version) { String key = getKeyIdVer(objectId, version); + ObjectModel objectModel = models.get(tenantId).get(key); + + if (objectModel == null) { + modelsLock.lock(); + try { + objectModel = models.get(tenantId).get(key); + if (objectModel == null) { + objectModel = getObjectModel(key); + models.get(tenantId).put(key, objectModel); + } + } finally { + modelsLock.unlock(); + } + } + + return objectModel; + } + + private ObjectModel getObjectModel(String key) { Optional tbResource = context.getTransportResourceCache().get(this.tenantId, LWM2M_MODEL, key); return tbResource.map(resource -> helper.parseFromXmlToObjectModel( Base64.getDecoder().decode(resource.getData()), diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java index e6fddf430b..38fc288aee 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/attributes/DefaultLwM2MAttributesService.java @@ -24,6 +24,7 @@ import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; +import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.springframework.stereotype.Service; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; @@ -85,6 +86,7 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService { private final LwM2mDownlinkMsgHandler downlinkHandler; private final LwM2MTelemetryLogService logService; private final LwM2MOtaUpdateService otaUpdateService; + private final LwM2mModelProvider modelProvider; @Override public ListenableFuture> getSharedAttributes(LwM2mClient client, Collection keys) { @@ -215,7 +217,7 @@ public class DefaultLwM2MAttributesService implements LwM2MAttributesService { clientContext.update(lwM2MClient); // #2.1 attributesUpdate.forEach((pathIdVer, tsKvProto) -> { - ResourceModel resourceModel = lwM2MClient.getResourceModel(pathIdVer, this.config.getModelProvider()); + ResourceModel resourceModel = lwM2MClient.getResourceModel(pathIdVer, modelProvider); Object newValProto = getValueFromKvProto(tsKvProto.getKv()); Object oldResourceValue = this.getResourceValueFormatKv(lwM2MClient, pathIdVer); if (!resourceModel.multiple || !(newValProto instanceof JsonElement)) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index f120bf84a2..cee6842192 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -441,7 +441,7 @@ public class LwM2mClient implements Serializable { this.lock = new ReentrantLock(); } - public long updateLastUplinkTime(){ + public long updateLastUplinkTime() { this.lastUplinkTime = System.currentTimeMillis(); this.firstEdrxDownlink = true; return lastUplinkTime; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 2c56a2d32b..827152db7b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; +import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.server.registration.Registration; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +38,7 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; +import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientStore; @@ -70,6 +72,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { private final TbLwM2MClientStore clientStore; private final LwM2MSessionManager sessionManager; private final TransportDeviceProfileCache deviceProfileCache; + private final LwM2mVersionedModelProvider modelProvider; @Autowired @Lazy @@ -534,4 +537,5 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } return timeout; } + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 3382c8a83b..037474aa1c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -65,6 +65,7 @@ import org.thingsboard.server.common.data.device.data.lwm2m.ObjectAttributes; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; +import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.common.LwM2MExecutorAwareService; @@ -112,6 +113,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private final LwM2MTransportServerConfig config; private final LwM2MTelemetryLogService logService; private final LwM2mClientContext clientContext; + private final LwM2mVersionedModelProvider modelProvider; @PostConstruct public void init() { @@ -136,13 +138,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @Override public void sendReadRequest(LwM2mClient client, TbLwM2MReadRequest request, DownlinkRequestCallback callback) { - try { - validateVersionedId(client, request); - ReadRequest downlink = new ReadRequest(getReadRequestContentFormat(client, request, this.config.getModelProvider()), request.getObjectId()); - sendSimpleRequest(client, downlink, request.getTimeout(), callback); - } catch (InvalidRequestException e) { - callback.onValidationError(request.toString(), e.getMessage()); - } + validateVersionedId(client, request); + ReadRequest downlink = new ReadRequest(getRequestContentFormat(client, request.getVersionedId(), modelProvider), request.getObjectId()); + sendSimpleRequest(client, downlink, request.getTimeout(), callback); } @Override @@ -164,7 +162,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im Set observations = context.getServer().getObservationService().getObservations(client.getRegistration()); if (observations.stream().noneMatch(observation -> observation.getPath().equals(resultIds))) { ObserveRequest downlink; - ContentFormat contentFormat = getReadRequestContentFormat(client, request, this.config.getModelProvider()); + ContentFormat contentFormat = getReadRequestContentFormat(client, request, modelProvider); if (resultIds.isResource()) { downlink = new ObserveRequest(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId()); } else if (resultIds.isObjectInstance()) { @@ -199,7 +197,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im try { validateVersionedId(client, request); LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(request.getVersionedId())); - ResourceModel resourceModelExecute = client.getResourceModel(request.getVersionedId(), this.config.getModelProvider()); + ResourceModel resourceModelExecute = client.getResourceModel(request.getVersionedId(), modelProvider); if (resourceModelExecute == null) { LwM2mModel model = createModelsDefault(); if (pathIds.isResource()) { @@ -294,9 +292,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im LwM2mPath resultIds = new LwM2mPath(request.getObjectId()); if (resultIds.isResource() || resultIds.isResourceInstance()) { validateVersionedId(client, request); - ResourceModel resourceModelWrite = client.getResourceModel(request.getVersionedId(), this.config.getModelProvider()); + ResourceModel resourceModelWrite = client.getResourceModel(request.getVersionedId(), modelProvider); if (resourceModelWrite != null) { - ContentFormat contentFormat = getWriteRequestContentFormat(client, request, this.config.getModelProvider()); + ContentFormat contentFormat = getWriteRequestContentFormat(client, request, modelProvider); try { WriteRequest downlink = null; String msgError = ""; @@ -359,7 +357,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im if (resultIds.isObjectInstance() || resultIds.isResource()) { validateVersionedId(client, request); WriteRequest downlink = null; - ContentFormat contentFormat = getWriteRequestContentFormat(client, request, this.config.getModelProvider()); + ContentFormat contentFormat = getWriteRequestContentFormat(client, request, modelProvider); String msgError = ""; if (resultIds.isObjectInstance()) { /* @@ -368,7 +366,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im * contentFormat – Format of the payload (TLV or JSON). */ Collection resources = client.getNewResourcesForInstance(request.getVersionedId(), - request.getValue(), this.config.getModelProvider(), this.converter); + request.getValue(), modelProvider, this.converter); if (resources.size() > 0) { downlink = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resources); @@ -376,7 +374,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im msgError = " No resources to update!"; } } else if (resultIds.isResource()) { - ResourceModel resourceModelWrite = client.getResourceModel(request.getVersionedId(), this.config.getModelProvider()); + ResourceModel resourceModelWrite = client.getResourceModel(request.getVersionedId(), modelProvider); if (resourceModelWrite != null) { if (resourceModelWrite.multiple) { try { @@ -414,23 +412,23 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im validateVersionedId(client, request); CreateRequest downlink = null; LwM2mPath resultIds = new LwM2mPath(request.getObjectId()); - ObjectModel objectModel = client.getObjectModel(request.getVersionedId(), this.config.getModelProvider()); + ObjectModel objectModel = client.getObjectModel(request.getVersionedId(), modelProvider); // POST /{Object ID}/{Object Instance ID} && Resources is Mandatory if (objectModel != null) { if (objectModel.multiple) { // LwM2M CBOR, SenML CBOR, SenML JSON, or TLV (see [LwM2M-CORE]) - ContentFormat contentFormat = getWriteRequestContentFormat(client, request, this.config.getModelProvider()); + ContentFormat contentFormat = getWriteRequestContentFormat(client, request, modelProvider); if (resultIds.isObject() || resultIds.isObjectInstance()) { Collection resources; if (resultIds.isObject()) { if (request.getValue() != null) { - resources = client.getNewResourcesForInstance(request.getVersionedId(), request.getValue(), this.config.getModelProvider(), this.converter); + resources = client.getNewResourcesForInstance(request.getVersionedId(), request.getValue(), modelProvider, this.converter); downlink = new CreateRequest(contentFormat, resultIds.getObjectId(), resources); } else if (request.getNodes() != null && request.getNodes().size() > 0) { Set instances = ConcurrentHashMap.newKeySet(); request.getNodes().forEach((key, value) -> { - Collection resourcesForInstance = client.getNewResourcesForInstance(request.getVersionedId(), value, this.config.getModelProvider(), this.converter); + Collection resourcesForInstance = client.getNewResourcesForInstance(request.getVersionedId(), value, modelProvider, this.converter); LwM2mObjectInstance instance = new LwM2mObjectInstance(Integer.parseInt(key), resourcesForInstance); instances.add(instance); }); @@ -439,7 +437,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } } else { - resources = client.getNewResourcesForInstance(request.getVersionedId(), request.getValue(), this.config.getModelProvider(), this.converter); + resources = client.getNewResourcesForInstance(request.getVersionedId(), request.getValue(), modelProvider, this.converter); LwM2mObjectInstance instance = new LwM2mObjectInstance(resultIds.getObjectInstanceId(), resources); downlink = new CreateRequest(contentFormat, resultIds.getObjectId(), instance); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java index 306547a966..2e212056f7 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/DefaultLwM2MRpcRequestHandler.java @@ -23,6 +23,7 @@ import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.request.ContentFormat; +import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; @@ -88,6 +89,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { private final LwM2mUplinkMsgHandler uplinkHandler; private final LwM2mDownlinkMsgHandler downlinkHandler; private final LwM2MTelemetryLogService logService; + private final LwM2mModelProvider modelProvider; @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { @@ -255,7 +257,7 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { RpcWriteReplaceRequest requestBody = JacksonUtil.fromString(requestMsg.getParams(), RpcWriteReplaceRequest.class); LwM2mPath path = new LwM2mPath(fromVersionedIdToObjectId(versionedId)); if (path.isResource()) { - ResourceModel resourceModel = client.getResourceModel(versionedId, this.config.getModelProvider()); + ResourceModel resourceModel = client.getResourceModel(versionedId, modelProvider); if (resourceModel != null && resourceModel.multiple) { try { Map value = convertMultiResourceValuesFromRpcBody(requestBody.getValue(), resourceModel.type, versionedId); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDownlinkRequestCallbackProxy.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDownlinkRequestCallbackProxy.java index acd88abee1..5361682491 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDownlinkRequestCallbackProxy.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/rpc/RpcDownlinkRequestCallbackProxy.java @@ -67,7 +67,7 @@ public abstract class RpcDownlinkRequestCallbackProxy implements DownlinkR @Override public void onError(String params, Exception e) { - if (e instanceof TimeoutException) { + if (e instanceof TimeoutException || e instanceof org.eclipse.leshan.core.request.exception.TimeoutException) { transportService.process(client.getSession(), this.request, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); } else if (!(e instanceof ClientSleepingException)) { sendRpcReplyOnError(e); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index c3dc1d61be..e60680e5fb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.device.data.lwm2m.ObjectAttributes; import org.thingsboard.server.common.data.device.data.lwm2m.OtherConfiguration; import org.thingsboard.server.common.data.device.data.lwm2m.TelemetryMappingConfiguration; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageUtil; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; @@ -62,6 +63,7 @@ import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.LwM2mOtaConvert; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper; +import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; import org.thingsboard.server.transport.lwm2m.server.attributes.LwM2MAttributesService; import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientStateException; @@ -143,8 +145,8 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private final LwM2mTransportServerHelper helper; private final TbLwM2MDtlsSessionStore sessionStore; private final LwM2mClientContext clientContext; - private final LwM2MRpcRequestHandler rpcHandler; private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; + private final LwM2mVersionedModelProvider modelProvider; public DefaultLwM2mUplinkMsgHandler(TransportService transportService, LwM2MTransportServerConfig config, @@ -154,9 +156,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2MSessionManager sessionManager, @Lazy LwM2MOtaUpdateService otaService, @Lazy LwM2MAttributesService attributesService, - @Lazy LwM2MRpcRequestHandler rpcHandler, @Lazy LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler, - LwM2mTransportContext context, TbLwM2MDtlsSessionStore sessionStore) { + LwM2mTransportContext context, + TbLwM2MDtlsSessionStore sessionStore, + LwM2mVersionedModelProvider modelProvider) { this.transportService = transportService; this.sessionManager = sessionManager; this.attributesService = attributesService; @@ -165,10 +168,10 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl this.helper = helper; this.clientContext = clientContext; this.logService = logService; - this.rpcHandler = rpcHandler; this.defaultLwM2MDownlinkMsgHandler = defaultLwM2MDownlinkMsgHandler; this.context = context; this.sessionStore = sessionStore; + this.modelProvider = modelProvider; } @PostConstruct @@ -313,7 +316,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl public void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response) { if (response.getContent() != null) { LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path, this.config.getModelProvider()); + ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path, modelProvider); if (objectModelVersion != null) { if (response.getContent() instanceof LwM2mObject) { LwM2mObject lwM2mObject = (LwM2mObject) response.getContent(); @@ -392,15 +395,19 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } @Override - public void onResourceUpdate(Optional resourceUpdateMsgOpt) { - String idVer = resourceUpdateMsgOpt.get().getResourceKey(); - clientContext.getLwM2mClients().forEach(e -> e.updateResourceModel(idVer, this.config.getModelProvider())); + public void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt) { + String idVer = resourceUpdateMsgOpt.getResourceKey(); + TenantId tenantId = new TenantId(new UUID(resourceUpdateMsgOpt.getTenantIdMSB(), resourceUpdateMsgOpt.getTenantIdLSB())); + modelProvider.evict(tenantId, idVer); + clientContext.getLwM2mClients().forEach(e -> e.updateResourceModel(idVer, modelProvider)); } @Override - public void onResourceDelete(Optional resourceDeleteMsgOpt) { - String pathIdVer = resourceDeleteMsgOpt.get().getResourceKey(); - clientContext.getLwM2mClients().forEach(e -> e.deleteResources(pathIdVer, this.config.getModelProvider())); + public void onResourceDelete(TransportProtos.ResourceDeleteMsg resourceDeleteMsgOpt) { + String pathIdVer = resourceDeleteMsgOpt.getResourceKey(); + TenantId tenantId = new TenantId(new UUID(resourceDeleteMsgOpt.getTenantIdMSB(), resourceDeleteMsgOpt.getTenantIdLSB())); + modelProvider.evict(tenantId, pathIdVer); + clientContext.getLwM2mClients().forEach(e -> e.deleteResources(pathIdVer, modelProvider)); } /** @@ -541,7 +548,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl */ private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path, Mode mode, int code) { Registration registration = lwM2MClient.getRegistration(); - if (lwM2MClient.saveResourceValue(path, lwM2mResource, this.config.getModelProvider(), mode)) { + if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, registration))) { otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, registration))) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index af0d349687..2d8139bf9b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -49,9 +49,9 @@ public interface LwM2mUplinkMsgHandler { void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt); - void onResourceUpdate(Optional resourceUpdateMsgOpt); + void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt); - void onResourceDelete(Optional resourceDeleteMsgOpt); + void onResourceDelete(TransportProtos.ResourceDeleteMsg resourceDeleteMsgOpt); void onAwakeDev(Registration registration); diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java index 1335b6105b..ef3db6cf84 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportContext.java @@ -31,6 +31,7 @@ import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by ashvayka on 04.10.18. @@ -71,4 +72,20 @@ public class MqttTransportContext extends TransportContext { @Getter @Value("${transport.mqtt.timeout:10000}") private long timeout; + + private final AtomicInteger connectionsCounter = new AtomicInteger(); + + @PostConstruct + public void init() { + super.init(); + transportService.createGaugeStats("openConnections", connectionsCounter); + } + + public void channelRegistered() { + connectionsCounter.incrementAndGet(); + } + + public void channelUnregistered() { + connectionsCounter.decrementAndGet(); + } } 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 d8d06a3173..35e6e8c508 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 @@ -101,8 +101,6 @@ import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.FAILURE; -import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN; -import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN; /** * @author Andrew Shvayka @@ -110,8 +108,8 @@ import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVIC @Slf4j public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener>, SessionMsgListener { - private static final Pattern FW_REQUEST_PATTERN = Pattern.compile(DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN); - private static final Pattern SW_REQUEST_PATTERN = Pattern.compile(DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN); + private static final Pattern FW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_FIRMWARE_REQUEST_TOPIC_PATTERN); + private static final Pattern SW_REQUEST_PATTERN = Pattern.compile(MqttTopics.DEVICE_SOFTWARE_REQUEST_TOPIC_PATTERN); private static final String PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"; @@ -133,6 +131,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final ConcurrentHashMap chunkSizes; private final ConcurrentMap rpcAwaitingAck; + private TopicType attrSubTopicType; + private TopicType rpcSubTopicType; + private TopicType attrReqTopicType; + private TopicType toServerRpcSubTopicType; + MqttTransportHandler(MqttTransportContext context, SslHandler sslHandler) { this.sessionId = UUID.randomUUID(); this.context = context; @@ -146,6 +149,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.rpcAwaitingAck = new ConcurrentHashMap<>(); } + @Override + public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + super.channelRegistered(ctx); + context.channelRegistered(); + } + + @Override + public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + super.channelUnregistered(ctx); + context.channelUnregistered(); + } + @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { log.trace("[{}] Processing msg: {}", sessionId, msg); @@ -343,14 +358,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement TransportProtos.PostTelemetryMsg postTelemetryMsg = payloadAdaptor.convertToPostTelemetry(deviceSessionCtx, mqttMsg); transportService.process(deviceSessionCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(ctx, msgId, postTelemetryMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX)) { - TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg); + TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); + attrReqTopicType = TopicType.V1; } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_TOPIC)) { - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_TOPIC)) { - TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg); + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); + toServerRpcSubTopicType = TopicType.V1; } else if (topicName.equals(MqttTopics.DEVICE_CLAIM_TOPIC)) { TransportProtos.ClaimDeviceMsg claimDeviceMsg = payloadAdaptor.convertToClaimDevice(deviceSessionCtx, mqttMsg); transportService.process(deviceSessionCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(ctx, msgId, claimDeviceMsg)); @@ -358,6 +375,57 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement getOtaPackageCallback(ctx, mqttMsg, msgId, fwMatcher, OtaPackageType.FIRMWARE); } else if ((fwMatcher = SW_REQUEST_PATTERN.matcher(topicName)).find()) { 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)); + } 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)); + } 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)); + } 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)); + } 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)); + } 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)); + } 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)); + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC)) { + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = context.getProtoMqttAdaptor().convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC)) { + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = payloadAdaptor.convertToDeviceRpcResponse(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(ctx, msgId, rpcResponseMsg)); + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC)) { + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = context.getJsonMqttAdaptor().convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); + toServerRpcSubTopicType = TopicType.V2_JSON; + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC)) { + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = context.getProtoMqttAdaptor().convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); + toServerRpcSubTopicType = TopicType.V2_PROTO; + } else if (topicName.startsWith(MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC)) { + TransportProtos.ToServerRpcRequestMsg rpcRequestMsg = payloadAdaptor.convertToServerRpcRequest(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC); + transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequestMsg, getPubAckCallback(ctx, msgId, rpcRequestMsg)); + toServerRpcSubTopicType = TopicType.V2; + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX)) { + TransportProtos.GetAttributeRequestMsg getAttributeMsg = context.getJsonMqttAdaptor().convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); + attrReqTopicType = TopicType.V2_JSON; + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX)) { + TransportProtos.GetAttributeRequestMsg getAttributeMsg = context.getProtoMqttAdaptor().convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX); + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); + attrReqTopicType = TopicType.V2_PROTO; + } else if (topicName.startsWith(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX)) { + TransportProtos.GetAttributeRequestMsg getAttributeMsg = payloadAdaptor.convertToGetAttributes(deviceSessionCtx, mqttMsg, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX); + transportService.process(deviceSessionCtx.getSessionInfo(), getAttributeMsg, getPubAckCallback(ctx, msgId, getAttributeMsg)); + attrReqTopicType = TopicType.V2; } else { transportService.reportActivity(deviceSessionCtx.getSessionInfo()); ack(ctx, msgId); @@ -529,19 +597,53 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement try { switch (topic) { case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); - registerSubQoS(topic, grantedQoSList, reqQoS); + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); activityReported = true; break; } case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); - registerSubQoS(topic, grantedQoSList, reqQoS); + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); activityReported = true; break; } case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: case MqttTopics.GATEWAY_RPC_TOPIC: case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: @@ -568,6 +670,18 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList)); } + private void processRpcSubscribe(List grantedQoSList, String topic, MqttQoS reqQoS, TopicType topicType) { + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToRPCMsg.newBuilder().build(), null); + rpcSubTopicType = topicType; + registerSubQoS(topic, grantedQoSList, reqQoS); + } + + private void processAttributesSubscribe(List grantedQoSList, String topic, MqttQoS reqQoS, TopicType topicType) { + transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), null); + attrSubTopicType = topicType; + registerSubQoS(topic, grantedQoSList, reqQoS); + } + private void registerSubQoS(String topic, List grantedQoSList, MqttQoS reqQoS) { grantedQoSList.add(getMinSupportedQos(reqQoS)); mqttQoSMap.put(new MqttTopicMatcher(topic), getMinSupportedQos(reqQoS)); @@ -583,18 +697,43 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement mqttQoSMap.remove(new MqttTopicMatcher(topicName)); try { switch (topicName) { - case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { + case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().setUnsubscribe(true).build(), null); activityReported = true; break; } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { transportService.process(deviceSessionCtx.getSessionInfo(), TransportProtos.SubscribeToRPCMsg.newBuilder().setUnsubscribe(true).build(), null); activityReported = true; break; } + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: + case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: + case MqttTopics.GATEWAY_RPC_TOPIC: + case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: + case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: { + activityReported = true; + break; + } } } catch (Exception e) { log.warn("[{}] Failed to process unsubscription [{}] to [{}]", sessionId, mqttMsg.variableHeader().messageId(), topicName); @@ -717,6 +856,10 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { log.error("[{}] Unexpected Exception", sessionId, cause); ctx.close(); + if (cause instanceof OutOfMemoryError) { + log.error("Received critical error. Going to shutdown the service."); + System.exit(1); + } } private static MqttSubAckMessage createSubAckMessage(Integer msgId, List grantedQoSList) { @@ -821,8 +964,29 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg response) { + log.trace("[{}] Received get attributes response", sessionId); + String topicBase; + MqttTransportAdaptor adaptor; + switch (attrReqTopicType) { + case V2: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + topicBase = MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX; + break; + case V2_JSON: + adaptor = context.getJsonMqttAdaptor(); + topicBase = MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_JSON_TOPIC_PREFIX; + break; + case V2_PROTO: + adaptor = context.getProtoMqttAdaptor(); + topicBase = MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_PROTO_TOPIC_PREFIX; + break; + default: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + topicBase = MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX; + break; + } try { - deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, response).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + adaptor.convertToPublish(deviceSessionCtx, response, topicBase).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes response to MQTT msg", sessionId, e); } @@ -831,8 +995,29 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { log.trace("[{}] Received attributes update notification to device", sessionId); + log.info("[{}] : attrSubTopicType: {}", notification.toString(), attrSubTopicType); + String topic; + MqttTransportAdaptor adaptor; + switch (attrSubTopicType) { + case V2: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + topic = MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC; + break; + case V2_JSON: + adaptor = context.getJsonMqttAdaptor(); + topic = MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC; + break; + case V2_PROTO: + adaptor = context.getProtoMqttAdaptor(); + topic = MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC; + break; + default: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + topic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; + break; + } try { - deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, notification).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); } @@ -846,9 +1031,29 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - log.trace("[{}] Received RPC command to device", sessionId); + log.info("[{}] Received RPC command to device", sessionId); + String baseTopic; + MqttTransportAdaptor adaptor; + switch (rpcSubTopicType) { + case V2: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC; + break; + case V2_JSON: + adaptor = context.getJsonMqttAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC; + break; + case V2_PROTO: + adaptor = context.getProtoMqttAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC; + break; + default: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_REQUESTS_TOPIC; + break; + } try { - deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcRequest).ifPresent(payload -> { + adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic).ifPresent(payload -> { int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); if (isAckExpected(payload)) { rpcAwaitingAck.put(msgId, rpcRequest); @@ -882,9 +1087,29 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { - log.trace("[{}] Received RPC command to server", sessionId); + log.trace("[{}] Received RPC response from server", sessionId); + String baseTopic; + MqttTransportAdaptor adaptor; + switch (toServerRpcSubTopicType) { + case V2: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC; + break; + case V2_JSON: + adaptor = context.getJsonMqttAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC; + break; + case V2_PROTO: + adaptor = context.getProtoMqttAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC; + break; + default: + adaptor = deviceSessionCtx.getPayloadAdaptor(); + baseTopic = MqttTopics.DEVICE_RPC_RESPONSE_TOPIC; + break; + } try { - deviceSessionCtx.getPayloadAdaptor().convertToPublish(deviceSessionCtx, rpcResponse).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + adaptor.convertToPublish(deviceSessionCtx, rpcResponse, baseTopic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); } @@ -907,4 +1132,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { deviceSessionCtx.onDeviceUpdate(sessionInfo, device, deviceProfileOpt); } + + private enum TopicType { + V1, V2, V2_JSON, V2_PROTO + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 19d2e15fc5..dc2b74a8ea 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -64,6 +64,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { try { return JsonConverter.convertToTelemetryProto(new JsonParser().parse(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { + log.warn("Failed to decode post telemetry request", ex); throw new AdaptorException(ex); } } @@ -74,6 +75,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { try { return JsonConverter.convertToAttributesProto(new JsonParser().parse(payload)); } catch (IllegalStateException | JsonSyntaxException ex) { + log.warn("Failed to decode post attributes request", ex); throw new AdaptorException(ex); } } @@ -84,6 +86,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { try { return JsonConverter.convertToClaimDeviceProto(ctx.getDeviceId(), payload); } catch (IllegalStateException | JsonSyntaxException ex) { + log.warn("Failed to decode claim device request", ex); throw new AdaptorException(ex); } } @@ -99,33 +102,33 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } @Override - public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { - return processGetAttributeRequestMsg(inbound, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { + return processGetAttributeRequestMsg(inbound, topicBase); } @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { - return processToDeviceRpcResponseMsg(inbound, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { + return processToDeviceRpcResponseMsg(inbound, topicBase); } @Override - public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { - return processToServerRpcRequestMsg(ctx, inbound, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { + return processToServerRpcRequestMsg(ctx, inbound, topicBase); } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { - return processConvertFromAttributeResponseMsg(ctx, responseMsg, MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { + return processConvertFromAttributeResponseMsg(ctx, responseMsg, topicBase); } @Override public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { - return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC); + return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, JsonConverter.toJson(notificationMsg))); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { + return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg))); } @Override @@ -135,8 +138,8 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, String topicBase) { + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcRequest.getRequestId(), JsonConverter.toJson(rpcRequest, false))); } @Override @@ -145,8 +148,8 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse, String topicBase) { + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcResponse.getRequestId(), JsonConverter.toJson(rpcResponse))); } @Override @@ -169,11 +172,11 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - protected TransportProtos.GetAttributeRequestMsg processGetAttributeRequestMsg(MqttPublishMessage inbound, String topic) throws AdaptorException { + private TransportProtos.GetAttributeRequestMsg processGetAttributeRequestMsg(MqttPublishMessage inbound, String topicBase) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); try { TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(getRequestId(topicName, topic)); + result.setRequestId(getRequestId(topicName, topicBase)); String payload = inbound.payload().toString(UTF8); JsonElement requestBody = new JsonParser().parse(payload); Set clientKeys = toStringSet(requestBody, "clientKeys"); @@ -191,49 +194,50 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor { } } - protected TransportProtos.ToDeviceRpcResponseMsg processToDeviceRpcResponseMsg(MqttPublishMessage inbound, String topic) throws AdaptorException { + private TransportProtos.ToDeviceRpcResponseMsg processToDeviceRpcResponseMsg(MqttPublishMessage inbound, String topicBase) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); try { - int requestId = getRequestId(topicName, topic); + int requestId = getRequestId(topicName, topicBase); String payload = inbound.payload().toString(UTF8); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(payload).build(); } catch (RuntimeException e) { - log.warn("Failed to decode Rpc response", e); + log.warn("Failed to decode rpc response", e); throw new AdaptorException(e); } } - protected TransportProtos.ToServerRpcRequestMsg processToServerRpcRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topic) throws AdaptorException { + private TransportProtos.ToServerRpcRequestMsg processToServerRpcRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { String topicName = inbound.variableHeader().topicName(); String payload = validatePayload(ctx.getSessionId(), inbound.payload(), false); try { - int requestId = getRequestId(topicName, topic); + int requestId = getRequestId(topicName, topicBase); return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId); } catch (IllegalStateException | JsonSyntaxException ex) { + log.warn("Failed to decode to server rpc request", ex); throw new AdaptorException(ex); } } - protected Optional processConvertFromAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topic) throws AdaptorException { + private Optional processConvertFromAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { int requestId = responseMsg.getRequestId(); if (requestId >= 0) { return Optional.of(createMqttPublishMsg(ctx, - topic + requestId, + topicBase + requestId, JsonConverter.toJson(responseMsg))); } return Optional.empty(); } } - protected Optional processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg, String topic) throws AdaptorException { + private Optional processConvertFromGatewayAttributeResponseMsg(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { JsonObject result = JsonConverter.getJsonObjectForGateway(deviceName, responseMsg); - return Optional.of(createMqttPublishMsg(ctx, topic, result)); + return Optional.of(createMqttPublishMsg(ctx, MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, result)); } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java index 62ae6ae027..53ef439eda 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java @@ -52,27 +52,27 @@ public interface MqttTransportAdaptor { PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; - GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; + GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException; - ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException; + ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException; - ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException; + ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException; ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; - Optional convertToPublish(MqttDeviceAwareSessionContext ctx, GetAttributeResponseMsg responseMsg) throws AdaptorException; + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException; Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, GetAttributeResponseMsg responseMsg) throws AdaptorException; - Optional convertToPublish(MqttDeviceAwareSessionContext ctx, AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, AttributeUpdateNotificationMsg notificationMsg, String topic) throws AdaptorException; Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; - Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException; + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ToDeviceRpcRequestMsg rpcRequest, String topicBase) throws AdaptorException; Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException; - Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse) throws AdaptorException; + Optional convertToPublish(MqttDeviceAwareSessionContext ctx, ToServerRpcResponseMsg rpcResponse, String topicBase) throws AdaptorException; ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index a007c73a5a..4fa47b367d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -20,7 +20,6 @@ import com.google.gson.JsonParser; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.util.JsonFormat; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; @@ -49,10 +48,11 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = toBytes(inbound.payload()); - Descriptors.Descriptor telemetryDynamicMsgDescriptor = getDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); + Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); try { - return JsonConverter.convertToTelemetryProto(new JsonParser().parse(dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); + return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); } catch (Exception e) { + log.warn("Failed to decode post telemetry request", e); throw new AdaptorException(e); } } @@ -61,10 +61,11 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; byte[] bytes = toBytes(inbound.payload()); - Descriptors.Descriptor attributesDynamicMessageDescriptor = getDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); + Descriptors.Descriptor attributesDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getAttributesDynamicMessageDescriptor()); try { - return JsonConverter.convertToAttributesProto(new JsonParser().parse(dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); + return JsonConverter.convertToAttributesProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, attributesDynamicMessageDescriptor))); } catch (Exception e) { + log.warn("Failed to decode post attributes request", e); throw new AdaptorException(e); } } @@ -75,16 +76,17 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { try { return ProtoConverter.convertToClaimDeviceProto(ctx.getDeviceId(), bytes); } catch (InvalidProtocolBufferException e) { + log.warn("Failed to decode claim device request", e); throw new AdaptorException(e); } } @Override - public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { byte[] bytes = toBytes(inbound.payload()); String topicName = inbound.variableHeader().topicName(); try { - int requestId = getRequestId(topicName, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + int requestId = getRequestId(topicName, topicBase); return ProtoConverter.convertToGetAttributeRequestMessage(bytes, requestId); } catch (InvalidProtocolBufferException e) { log.warn("Failed to decode get attributes request", e); @@ -93,29 +95,30 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { } @Override - public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; String topicName = mqttMsg.variableHeader().topicName(); byte[] bytes = toBytes(mqttMsg.payload()); - Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = getDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); + Descriptors.Descriptor rpcResponseDynamicMessageDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getRpcResponseDynamicMessageDescriptor()); try { - int requestId = getRequestId(topicName, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC); - JsonElement response = new JsonParser().parse(dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); + int requestId = getRequestId(topicName, topicBase); + JsonElement response = new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, rpcResponseDynamicMessageDescriptor)); return TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setPayload(response.toString()).build(); } catch (Exception e) { - log.warn("Failed to decode Rpc response", e); + log.warn("Failed to decode rpc response", e); throw new AdaptorException(e); } } @Override - public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg) throws AdaptorException { + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { byte[] bytes = toBytes(mqttMsg.payload()); String topicName = mqttMsg.variableHeader().topicName(); try { - int requestId = getRequestId(topicName, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC); + int requestId = getRequestId(topicName, topicBase); return ProtoConverter.convertToServerRpcRequest(bytes, requestId); } catch (InvalidProtocolBufferException e) { + log.warn("Failed to decode to server rpc request", e); throw new AdaptorException(e); } } @@ -126,40 +129,43 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { try { return ProtoConverter.convertToProvisionRequestMsg(bytes); } catch (InvalidProtocolBufferException ex) { + log.warn("Failed to decode provision request", ex); throw new AdaptorException(ex); } } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { if (!StringUtils.isEmpty(responseMsg.getError())) { throw new AdaptorException(responseMsg.getError()); } else { int requestId = responseMsg.getRequestId(); if (requestId >= 0) { - return Optional.of(createMqttPublishMsg(ctx, - MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId, - responseMsg.toByteArray())); + return Optional.of(createMqttPublishMsg(ctx, topicBase + requestId, responseMsg.toByteArray())); } return Optional.empty(); } } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, String topicBase) throws AdaptorException { DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; DynamicMessage.Builder rpcRequestDynamicMessageBuilder = deviceSessionCtx.getRpcRequestDynamicMessageBuilder(); - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder))); + if (rpcRequestDynamicMessageBuilder == null) { + throw new AdaptorException("Failed to get rpcRequestDynamicMessageBuilder!"); + } else { + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcRequest.getRequestId(), ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder))); + } } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC + rpcResponse.getRequestId(), rpcResponse.toByteArray())); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse, String topicBase) { + return Optional.of(createMqttPublishMsg(ctx, topicBase + rpcResponse.getRequestId(), rpcResponse.toByteArray())); } @Override - public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) { - return Optional.of(createMqttPublishMsg(ctx, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, notificationMsg.toByteArray())); + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { + return Optional.of(createMqttPublishMsg(ctx, topic, notificationMsg.toByteArray())); } @Override @@ -213,17 +219,4 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor { private int getRequestId(String topicName, String topic) { return Integer.parseInt(topicName.substring(topic.length())); } - - private Descriptors.Descriptor getDescriptor(Descriptors.Descriptor descriptor) throws AdaptorException { - if (descriptor == null) { - throw new AdaptorException("Failed to get dynamic message descriptor!"); - } - return descriptor; - } - - private String dynamicMsgToJson(byte[] bytes, Descriptors.Descriptor descriptor) throws InvalidProtocolBufferException { - DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, bytes); - return JsonFormat.printer().includingDefaultValueFields().print(dynamicMessage); - } - } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java index 156cff5f7d..205ad1e44d 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/SessionMsgListener.java @@ -56,7 +56,7 @@ public interface SessionMsgListener { default void onDeviceDeleted(DeviceId deviceId) {} - default void onResourceUpdate(Optional resourceUpdateMsgOpt) {} + default void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt) {} - default void onResourceDelete(Optional resourceUpdateMsgOpt) {} + default void onResourceDelete(TransportProtos.ResourceDeleteMsg resourceUpdateMsgOpt) {} } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java index d48d3fd04e..ec21851b84 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportContext.java @@ -39,7 +39,7 @@ public abstract class TransportContext { protected final ObjectMapper mapper = new ObjectMapper(); @Autowired - private TransportService transportService; + protected TransportService transportService; @Autowired private TbServiceInfoProvider serviceInfoProvider; 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 28ad7ddb9b..aff9543286 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 @@ -58,6 +58,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenR import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by ashvayka on 04.10.18. @@ -138,4 +139,6 @@ public interface TransportService { ExecutorService getCallbackExecutor(); boolean hasSession(SessionInfoProto sessionInfo); + + void createGaugeStats(String openConnections, AtomicInteger connectionsCounter); } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java index 39d4db069e..0d1348b462 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -19,6 +19,7 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; @@ -190,4 +191,17 @@ public class ProtoConverter { throw new AdaptorException("Failed to convert ToDeviceRpcRequestMsg to Dynamic Rpc request message due to: ", e); } } + + public static Descriptors.Descriptor validateDescriptor(Descriptors.Descriptor descriptor) throws AdaptorException { + if (descriptor == null) { + throw new AdaptorException("Failed to get dynamic message descriptor!"); + } + return descriptor; + } + + public static String dynamicMsgToJson(byte[] bytes, Descriptors.Descriptor descriptor) throws InvalidProtocolBufferException { + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, bytes); + return JsonFormat.printer().includingDefaultValueFields().print(dynamicMessage); + } + } 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 dbe42cf9f4..f532f52445 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 @@ -21,9 +21,11 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.protobuf.ByteString; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -96,6 +98,7 @@ import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -110,6 +113,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * Created by ashvayka on 17.10.18. @@ -135,6 +139,10 @@ public class DefaultTransportService implements TransportService { private long clientSideRpcTimeout; @Value("${queue.transport.poll_interval}") private int notificationsPollDuration; + @Value("${transport.stats.enabled:false}") + private boolean statsEnabled; + + private final Map statsMap = new LinkedHashMap<>(); private final Gson gson = new Gson(); private final TbTransportQueueFactory queueProvider; @@ -899,7 +907,8 @@ public class DefaultTransportService implements TransportService { String resourceId = msg.getResourceKey(); transportResourceCache.update(tenantId, resourceType, resourceId); sessions.forEach((id, mdRez) -> { - transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceUpdate(Optional.ofNullable(msg))); + log.trace("ResourceUpdate - [{}] [{}]", id, mdRez); + transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceUpdate(msg)); }); } else if (toSessionMsg.hasResourceDeleteMsg()) { @@ -910,7 +919,7 @@ public class DefaultTransportService implements TransportService { transportResourceCache.evict(tenantId, resourceType, resourceId); sessions.forEach((id, mdRez) -> { log.warn("ResourceDelete - [{}] [{}]", id, mdRez); - transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(Optional.ofNullable(msg))); + transportCallbackExecutor.submit(() -> mdRez.getListener().onResourceDelete(msg)); }); } else { //TODO: should we notify the device actor about missed session? @@ -1166,4 +1175,19 @@ public class DefaultTransportService implements TransportService { public boolean hasSession(TransportProtos.SessionInfoProto sessionInfo) { return sessions.containsKey(toSessionId(sessionInfo)); } + + @Override + public void createGaugeStats(String statsName, AtomicInteger number) { + statsFactory.createGauge(StatsType.TRANSPORT + "." + statsName, number); + statsMap.put(statsName, number); + } + + @Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}") + public void printStats() { + if (statsEnabled) { + String values = statsMap.entrySet().stream() + .map(kv -> kv.getKey() + " [" + kv.getValue() + "]").collect(Collectors.joining(", ")); + log.info("Transport Stats: {}", values); + } + } } diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 5480a3bd69..d615890507 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -20,6 +20,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -53,4 +54,15 @@ public class DonAsynchron { Futures.addCallback(future, callback, MoreExecutors.directExecutor()); } } + + public static ListenableFuture submit(Callable task, Consumer onSuccess, Consumer onFailure, Executor executor) { + return submit(task, onSuccess, onFailure, executor, null); + } + + public static ListenableFuture submit(Callable task, Consumer onSuccess, Consumer onFailure, Executor executor, Executor callbackExecutor) { + ListenableFuture future = Futures.submit(task, executor); + withCallback(future, onSuccess, onFailure, callbackExecutor); + return future; + } + } diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardForkJoinWorkerThreadFactory.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardForkJoinWorkerThreadFactory.java index 319421f8bc..a4e56d579a 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardForkJoinWorkerThreadFactory.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardForkJoinWorkerThreadFactory.java @@ -34,8 +34,8 @@ public class ThingsBoardForkJoinWorkerThreadFactory implements ForkJoinPool.Fork @Override public final ForkJoinWorkerThread newThread(ForkJoinPool pool) { ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool); + thread.setContextClassLoader(this.getClass().getClassLoader()); thread.setName(namePrefix +"-"+thread.getPoolIndex()+"-"+threadNumber.getAndIncrement()); return thread; } - } diff --git a/dao/pom.xml b/dao/pom.xml index 922b389bff..f490a0e8e5 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -201,6 +201,11 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-test + test + org.springframework spring-test @@ -211,6 +216,16 @@ hsqldb test + + org.testcontainers + postgresql + test + + + org.testcontainers + jdbc + test + org.springframework spring-context-support @@ -239,7 +254,14 @@ maven-surefire-plugin ${surfire.version} + + **/sql/*Test.java + **/sql/*/*Test.java + **/psql/*Test.java + **/nosql/*Test.java + + **/*Test.java **/*TestSuite.java diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java index 59ed00052e..7e07347ca4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java @@ -35,6 +35,8 @@ public interface DeviceCredentialsDao extends Dao { */ DeviceCredentials save(TenantId tenantId, DeviceCredentials deviceCredentials); + DeviceCredentials saveAndFlush(TenantId tenantId, DeviceCredentials deviceCredentials); + /** * Find device credentials by device id. * 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 a5ed9e1e29..7a778762b8 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 @@ -95,7 +95,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); try { - return deviceCredentialsDao.save(tenantId, deviceCredentials); + return deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java index 120bf0bad0..2200416e6d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java @@ -30,6 +30,8 @@ public interface DeviceProfileDao extends Dao { DeviceProfile save(TenantId tenantId, DeviceProfile deviceProfile); + DeviceProfile saveAndFlush(TenantId tenantId, DeviceProfile deviceProfile); + PageData findDeviceProfiles(TenantId tenantId, PageLink pageLink); PageData findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 913ee19d1c..f3e2f6599f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -168,7 +168,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } DeviceProfile savedDeviceProfile; try { - savedDeviceProfile = deviceProfileDao.save(deviceProfile.getTenantId(), deviceProfile); + savedDeviceProfile = deviceProfileDao.saveAndFlush(deviceProfile.getTenantId(), deviceProfile); } catch (Exception t) { ConstraintViolationException e = extractConstraintViolationException(t).orElse(null); if (e != null && e.getConstraintName() != null && e.getConstraintName().equalsIgnoreCase("device_profile_name_unq_key")) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java index 2aad852c89..b30d40470b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EdgeEventEntity.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.model.sql; -import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; @@ -40,15 +39,14 @@ import javax.persistence.Table; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ACTION_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_COLUMN_FAMILY_NAME; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_EDGE_ID_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_BODY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_ENTITY_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EDGE_EVENT_UID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.EPOCH_DIFF; -import static org.thingsboard.server.dao.model.ModelConstants.EVENT_UID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.TS_COLUMN; @Data diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java index c121073d55..4cfb4b6dc9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.cassandra.guava.GuavaSession; +import org.thingsboard.server.dao.util.BufferedRateExecutor; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -40,7 +41,10 @@ public abstract class CassandraAbstractDao { private ConcurrentMap preparedStatementMap = new ConcurrentHashMap<>(); @Autowired - private CassandraBufferedRateExecutor rateLimiter; + private CassandraBufferedRateReadExecutor rateReadLimiter; + + @Autowired + private CassandraBufferedRateWriteExecutor rateWriteLimiter; private GuavaSession session; @@ -61,36 +65,38 @@ public abstract class CassandraAbstractDao { } protected AsyncResultSet executeRead(TenantId tenantId, Statement statement) { - return execute(tenantId, statement, defaultReadLevel); + return execute(tenantId, statement, defaultReadLevel, rateReadLimiter); } protected AsyncResultSet executeWrite(TenantId tenantId, Statement statement) { - return execute(tenantId, statement, defaultWriteLevel); + return execute(tenantId, statement, defaultWriteLevel, rateWriteLimiter); } protected TbResultSetFuture executeAsyncRead(TenantId tenantId, Statement statement) { - return executeAsync(tenantId, statement, defaultReadLevel); + return executeAsync(tenantId, statement, defaultReadLevel, rateReadLimiter); } protected TbResultSetFuture executeAsyncWrite(TenantId tenantId, Statement statement) { - return executeAsync(tenantId, statement, defaultWriteLevel); + return executeAsync(tenantId, statement, defaultWriteLevel, rateWriteLimiter); } - private AsyncResultSet execute(TenantId tenantId, Statement statement, ConsistencyLevel level) { + private AsyncResultSet execute(TenantId tenantId, Statement statement, ConsistencyLevel level, + BufferedRateExecutor rateExecutor) { if (log.isDebugEnabled()) { log.debug("Execute cassandra statement {}", statementToString(statement)); } - return executeAsync(tenantId, statement, level).getUninterruptibly(); + return executeAsync(tenantId, statement, level, rateExecutor).getUninterruptibly(); } - private TbResultSetFuture executeAsync(TenantId tenantId, Statement statement, ConsistencyLevel level) { + private TbResultSetFuture executeAsync(TenantId tenantId, Statement statement, ConsistencyLevel level, + BufferedRateExecutor rateExecutor) { if (log.isDebugEnabled()) { log.debug("Execute cassandra async statement {}", statementToString(statement)); } if (statement.getConsistencyLevel() == null) { statement.setConsistencyLevel(level); } - return rateLimiter.submit(new CassandraStatementTask(tenantId, getSession(), statement)); + return rateExecutor.submit(new CassandraStatementTask(tenantId, getSession(), statement)); } private static String statementToString(Statement statement) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java similarity index 50% rename from dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java rename to dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java index ac61a73f0c..839ca16483 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java @@ -22,9 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.stats.DefaultCounter; -import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; @@ -32,8 +29,6 @@ import org.thingsboard.server.dao.util.AsyncTaskContext; import org.thingsboard.server.dao.util.NoSqlAnyDao; import javax.annotation.PreDestroy; -import java.util.HashMap; -import java.util.Map; /** * Created by ashvayka on 24.10.18. @@ -41,15 +36,11 @@ import java.util.Map; @Component @Slf4j @NoSqlAnyDao -public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor { +public class CassandraBufferedRateReadExecutor extends AbstractBufferedRateExecutor { - @Autowired - private EntityService entityService; - private Map tenantNamesCache = new HashMap<>(); + static final String BUFFER_NAME = "Read"; - private boolean printTenantNames; - - public CassandraBufferedRateExecutor( + public CassandraBufferedRateReadExecutor( @Value("${cassandra.query.buffer_size}") int queueLimit, @Value("${cassandra.query.concurrent_limit}") int concurrencyLimit, @Value("${cassandra.query.permit_max_wait_time}") long maxWaitTime, @@ -60,57 +51,16 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor< @Value("${cassandra.query.tenant_rate_limits.configuration}") String tenantRateLimitsConfiguration, @Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames, @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq, - @Autowired StatsFactory statsFactory) { - super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration, printQueriesFreq, statsFactory); - this.printTenantNames = printTenantNames; + @Autowired StatsFactory statsFactory, + @Autowired EntityService entityService) { + super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration, printQueriesFreq, statsFactory, + entityService, printTenantNames); } @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") + @Override public void printStats() { - int queueSize = getQueueSize(); - int rateLimitedTenantsCount = (int) stats.getRateLimitedTenants().values().stream() - .filter(defaultCounter -> defaultCounter.get() > 0) - .count(); - - if (queueSize > 0 - || rateLimitedTenantsCount > 0 - || concurrencyLevel.get() > 0 - || stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0) - ) { - StringBuilder statsBuilder = new StringBuilder(); - - statsBuilder.append("queueSize").append(" = [").append(queueSize).append("] "); - stats.getStatsCounters().forEach(counter -> { - statsBuilder.append(counter.getName()).append(" = [").append(counter.get()).append("] "); - }); - statsBuilder.append("totalRateLimitedTenants").append(" = [").append(rateLimitedTenantsCount).append("] "); - statsBuilder.append(CONCURRENCY_LEVEL).append(" = [").append(concurrencyLevel.get()).append("] "); - - stats.getStatsCounters().forEach(StatsCounter::clear); - log.info("Permits {}", statsBuilder); - } - - stats.getRateLimitedTenants().entrySet().stream() - .filter(entry -> entry.getValue().get() > 0) - .forEach(entry -> { - TenantId tenantId = entry.getKey(); - DefaultCounter counter = entry.getValue(); - int rateLimitedRequests = counter.get(); - counter.clear(); - if (printTenantNames) { - String name = tenantNamesCache.computeIfAbsent(tenantId, tId -> { - try { - return entityService.fetchEntityNameAsync(TenantId.SYS_TENANT_ID, tenantId).get(); - } catch (Exception e) { - log.error("[{}] Failed to get tenant name", tenantId, e); - return "N/A"; - } - }); - log.info("[{}][{}] Rate limited requests: {}", tenantId, name, rateLimitedRequests); - } else { - log.info("[{}] Rate limited requests: {}", tenantId, rateLimitedRequests); - } - }); + super.printStats(); } @PreDestroy @@ -118,6 +68,11 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor< super.stop(); } + @Override + public String getBufferName() { + return BUFFER_NAME; + } + @Override protected SettableFuture create() { return SettableFuture.create(); @@ -133,7 +88,7 @@ public class CassandraBufferedRateExecutor extends AbstractBufferedRateExecutor< CassandraStatementTask task = taskCtx.getTask(); return task.executeAsync( statement -> - this.submit(new CassandraStatementTask(task.getTenantId(), task.getSession(), statement)) + this.submit(new CassandraStatementTask(task.getTenantId(), task.getSession(), statement)) ); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java new file mode 100644 index 0000000000..990baa1e2a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java @@ -0,0 +1,95 @@ +/** + * Copyright © 2016-2021 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.nosql; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor; +import org.thingsboard.server.dao.util.AsyncTaskContext; +import org.thingsboard.server.dao.util.NoSqlAnyDao; + +import javax.annotation.PreDestroy; + +/** + * Created by ashvayka on 24.10.18. + */ +@Component +@Slf4j +@NoSqlAnyDao +public class CassandraBufferedRateWriteExecutor extends AbstractBufferedRateExecutor { + + static final String BUFFER_NAME = "Write"; + + public CassandraBufferedRateWriteExecutor( + @Value("${cassandra.query.buffer_size}") int queueLimit, + @Value("${cassandra.query.concurrent_limit}") int concurrencyLimit, + @Value("${cassandra.query.permit_max_wait_time}") long maxWaitTime, + @Value("${cassandra.query.dispatcher_threads:2}") int dispatcherThreads, + @Value("${cassandra.query.callback_threads:4}") int callbackThreads, + @Value("${cassandra.query.poll_ms:50}") long pollMs, + @Value("${cassandra.query.tenant_rate_limits.enabled}") boolean tenantRateLimitsEnabled, + @Value("${cassandra.query.tenant_rate_limits.configuration}") String tenantRateLimitsConfiguration, + @Value("${cassandra.query.tenant_rate_limits.print_tenant_names}") boolean printTenantNames, + @Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq, + @Autowired StatsFactory statsFactory, + @Autowired EntityService entityService) { + super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, tenantRateLimitsEnabled, tenantRateLimitsConfiguration, printQueriesFreq, statsFactory, + entityService, printTenantNames); + } + + @Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}") + @Override + public void printStats() { + super.printStats(); + } + + @PreDestroy + public void stop() { + super.stop(); + } + + @Override + public String getBufferName() { + return BUFFER_NAME; + } + + @Override + protected SettableFuture create() { + return SettableFuture.create(); + } + + @Override + protected TbResultSetFuture wrap(CassandraStatementTask task, SettableFuture future) { + return new TbResultSetFuture(future); + } + + @Override + protected ListenableFuture execute(AsyncTaskContext taskCtx) { + CassandraStatementTask task = taskCtx.getTask(); + return task.executeAsync( + statement -> + this.submit(new CassandraStatementTask(task.getTenantId(), task.getSession(), statement)) + ); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 3cb0445d78..9411f3be27 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.oauth2; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -224,7 +225,7 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se if (StringUtils.isEmpty(clientRegistration.getAccessTokenUri())) { throw new DataValidationException("Token uri should be specified!"); } - if (StringUtils.isEmpty(clientRegistration.getScope())) { + if (CollectionUtils.isEmpty(clientRegistration.getScope())) { throw new DataValidationException("Scope should be specified!"); } if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 15ddb9a390..b577022d57 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -27,6 +27,7 @@ import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Caching; +import org.springframework.dao.ConcurrencyFailureException; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -201,6 +202,7 @@ public class BaseRelationService implements RelationService { } relationDao.deleteOutboundRelations(tenantId, entityId); + } @Override @@ -262,10 +264,13 @@ public class BaseRelationService implements RelationService { boolean delete(TenantId tenantId, Cache cache, EntityRelation relation, boolean deleteFromDb) { cacheEviction(relation, cache); if (deleteFromDb) { - return relationDao.deleteRelation(tenantId, relation); - } else { - return false; + try { + return relationDao.deleteRelation(tenantId, relation); + } catch (ConcurrencyFailureException e) { + log.debug("Concurrency exception while deleting relations [{}]", relation, e); + } } + return false; } private void cacheEviction(EntityRelation relation, Cache cache) { 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 2ead3f4334..99772ddabd 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 @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -38,7 +39,6 @@ import org.thingsboard.server.common.data.id.RuleNodeId; 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.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; @@ -59,6 +59,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantDao; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -416,41 +417,46 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, RuleChainType type, boolean overwrite) { + public List importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) { List importResults = new ArrayList<>(); + setRandomRuleChainIds(ruleChainData); resetRuleNodeIds(ruleChainData.getMetadata()); resetRuleChainMetadataTenantIds(tenantId, ruleChainData.getMetadata()); - if (overwrite) { - List persistentRuleChains = findAllTenantRuleChains(tenantId, type); - for (RuleChain ruleChain : ruleChainData.getRuleChains()) { - ComponentLifecycleEvent lifecycleEvent; - Optional persistentRuleChainOpt = persistentRuleChains.stream().filter(rc -> rc.getName().equals(ruleChain.getName())).findFirst(); - if (persistentRuleChainOpt.isPresent()) { - setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), ruleChain.getId(), persistentRuleChainOpt.get().getId()); - ruleChain.setRoot(persistentRuleChainOpt.get().isRoot()); - lifecycleEvent = ComponentLifecycleEvent.UPDATED; - } else { - ruleChain.setRoot(false); - lifecycleEvent = ComponentLifecycleEvent.CREATED; + + for (RuleChain ruleChain : ruleChainData.getRuleChains()) { + RuleChainImportResult importResult = new RuleChainImportResult(); + + ruleChain.setTenantId(tenantId); + ruleChain.setRoot(false); + + if (overwrite) { + Collection existingRuleChains = ruleChainDao.findByTenantIdAndTypeAndName(tenantId, + Optional.ofNullable(ruleChain.getType()).orElse(RuleChainType.CORE), ruleChain.getName()); + Optional existingRuleChain = existingRuleChains.stream().findFirst(); + if (existingRuleChain.isPresent()) { + setNewRuleChainId(ruleChain, ruleChainData.getMetadata(), ruleChain.getId(), existingRuleChain.get().getId()); + ruleChain.setRoot(existingRuleChain.get().isRoot()); + importResult.setUpdated(true); } - ruleChain.setTenantId(tenantId); - ruleChainDao.save(tenantId, ruleChain); - importResults.add(new RuleChainImportResult(tenantId, ruleChain.getId(), lifecycleEvent)); } - } else { - if (!CollectionUtils.isEmpty(ruleChainData.getRuleChains())) { - ruleChainData.getRuleChains().forEach(rc -> { - rc.setTenantId(tenantId); - rc.setRoot(false); - RuleChain savedRc = ruleChainDao.save(tenantId, rc); - importResults.add(new RuleChainImportResult(tenantId, savedRc.getId(), ComponentLifecycleEvent.CREATED)); - }); + + try { + ruleChain = saveRuleChain(ruleChain); + } catch (Exception e) { + importResult.setError(ExceptionUtils.getRootCauseMessage(e)); } + + importResult.setTenantId(tenantId); + importResult.setRuleChainId(ruleChain.getId()); + importResult.setRuleChainName(ruleChain.getName()); + importResults.add(importResult); } - if (!CollectionUtils.isEmpty(ruleChainData.getMetadata())) { + + if (CollectionUtils.isNotEmpty(ruleChainData.getMetadata())) { ruleChainData.getMetadata().forEach(md -> saveRuleChainMetaData(tenantId, md)); } + return importResults; } @@ -475,7 +481,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } if (isTenantId) { ObjectNode objNode = (ObjectNode) node; - objNode.put("id", tenantId.getId().toString()); + if (objNode.has("id")) { + objNode.put("id", tenantId.getId().toString()); + } } else { for (JsonNode jsonNode : node) { searchTenantIdRecursive(tenantId, jsonNode); @@ -544,6 +552,9 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (!edge.getTenantId().equals(ruleChain.getTenantId())) { throw new DataValidationException("Can't assign ruleChain to edge from different tenant!"); } + if (!RuleChainType.EDGE.equals(ruleChain.getType())) { + throw new DataValidationException("Can't assign non EDGE ruleChain to edge!"); + } try { createRelation(tenantId, new EntityRelation(edgeId, ruleChainId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE)); } catch (Exception e) { @@ -723,4 +734,5 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC checkRuleNodesAndDelete(tenantId, entity.getId()); } }; + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 98ad0b34a7..03fd25e944 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.rule; +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.rule.RuleChain; @@ -22,6 +23,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.TenantEntityDao; +import java.util.Collection; import java.util.UUID; /** @@ -74,4 +76,7 @@ public interface RuleChainDao extends Dao, TenantEntityDao { * @return the list of rule chain objects */ PageData findAutoAssignToEdgeRuleChainsByTenantId(UUID tenantId, PageLink pageLink); + + Collection findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java index 02ae32bd3e..97c9913239 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.dao.sql.device; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity; import java.util.UUID; @@ -23,7 +23,7 @@ import java.util.UUID; /** * Created by Valerii Sosliuk on 5/6/2017. */ -public interface DeviceCredentialsRepository extends CrudRepository { +public interface DeviceCredentialsRepository extends JpaRepository { DeviceCredentialsEntity findByDeviceId(UUID deviceId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 1dc2af4ecf..311c73100c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.sql.device; 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.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; @@ -26,7 +27,7 @@ import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; import java.util.UUID; -public interface DeviceProfileRepository extends PagingAndSortingRepository { +public interface DeviceProfileRepository extends JpaRepository { @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java index 68eb7ab37a..26453a5e52 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.device; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.dao.DaoUtil; @@ -46,6 +47,14 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao findDeviceProfiles(TenantId tenantId, PageLink pageLink) { return DaoUtil.toPageData( diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java index 4962b2e8d2..24b4084e1f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeEventRepository.java @@ -31,10 +31,12 @@ public interface EdgeEventRepository extends PagingAndSortingRepository= :startTime) " + - "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + + "AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT(:textSearch, '%'))" ) Page findEdgeEventsByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, + @Param("textSearch") String textSearch, @Param("startTime") Long startTime, @Param("endTime") Long endTime, Pageable pageable); @@ -44,10 +46,12 @@ public interface EdgeEventRepository extends PagingAndSortingRepository= :startTime) " + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + - "AND e.edgeEventAction <> 'TIMESERIES_UPDATED'" + "AND e.edgeEventAction <> 'TIMESERIES_UPDATED' " + + "AND LOWER(e.edgeEventType) LIKE LOWER(CONCAT(:textSearch, '%'))" ) Page findEdgeEventsByTenantIdAndEdgeIdWithoutTimeseriesUpdated(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, + @Param("textSearch") String textSearch, @Param("startTime") Long startTime, @Param("endTime") Long endTime, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index 050d0181f0..7a5eb50def 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -39,6 +39,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -107,6 +108,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao implemen eventFilter.getEntityId(), eventFilter.getMsgType(), eventFilter.isError(), - eventFilter.getError(), + eventFilter.getErrorStr(), eventFilter.getDataSearch(), eventFilter.getMetadataSearch(), DaoUtil.toPageable(pageLink))); @@ -212,7 +208,7 @@ public class JpaBaseEventDao extends JpaAbstractDao implemen notNull(pageLink.getEndTime()), eventFilter.getServer(), eventFilter.getMethod(), - eventFilter.getError(), + eventFilter.getErrorStr(), DaoUtil.toPageable(pageLink)) ); } @@ -231,7 +227,7 @@ public class JpaBaseEventDao extends JpaAbstractDao implemen eventFilter.getEvent(), statusFilterEnabled, statusFilter, - eventFilter.getError(), + eventFilter.getErrorStr(), DaoUtil.toPageable(pageLink)) ); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index 7b3ea36cfa..ac78e80685 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.dao.sql.query; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; @@ -239,22 +241,38 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { public static EntityType[] RELATION_QUERY_ENTITY_TYPES = new EntityType[]{ EntityType.TENANT, EntityType.CUSTOMER, EntityType.USER, EntityType.DASHBOARD, EntityType.ASSET, EntityType.DEVICE, EntityType.ENTITY_VIEW}; - private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, relation_type, lvl) AS (" + - " SELECT from_id, from_type, to_id, to_type, relation_type, 1 as lvl" + - " FROM relation" + + private static final String HIERARCHICAL_QUERY_TEMPLATE = " FROM (WITH RECURSIVE related_entities(from_id, from_type, to_id, to_type, lvl, path) AS (" + + " SELECT from_id, from_type, to_id, to_type," + + " 1 as lvl," + + " ARRAY[$in_id] as path" + // initial path + " FROM relation " + " WHERE $in_id = :relation_root_id and $in_type = :relation_root_type and relation_type_group = 'COMMON'" + + " GROUP BY from_id, from_type, to_id, to_type, lvl, path" + " UNION ALL" + - " SELECT r.from_id, r.from_type, r.to_id, r.to_type, r.relation_type, lvl + 1" + + " SELECT r.from_id, r.from_type, r.to_id, r.to_type," + + " (re.lvl + 1) as lvl, " + + " (re.path || ARRAY[r.$in_id]) as path" + " FROM relation r" + " INNER JOIN related_entities re ON" + " r.$in_id = re.$out_id and r.$in_type = re.$out_type and" + - " relation_type_group = 'COMMON' %s)" + - " SELECT re.$out_id entity_id, re.$out_type entity_type, max(re.lvl) lvl" + - " from related_entities re" + + " relation_type_group = 'COMMON' " + + " AND r.$in_id NOT IN (SELECT * FROM unnest(re.path)) " + + " %s" + + " GROUP BY r.from_id, r.from_type, r.to_id, r.to_type, (re.lvl + 1), (re.path || ARRAY[r.$in_id])" + + " )" + + " SELECT re.$out_id entity_id, re.$out_type entity_type, max(r_int.lvl) lvl" + + " from related_entities r_int" + + " INNER JOIN relation re ON re.from_id = r_int.from_id AND re.from_type = r_int.from_type" + + " AND re.to_id = r_int.to_id AND re.to_type = r_int.to_type" + + " AND re.relation_type_group = 'COMMON'" + " %s GROUP BY entity_id, entity_type) entity"; private static final String HIERARCHICAL_TO_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "to").replace("$out", "from"); private static final String HIERARCHICAL_FROM_QUERY_TEMPLATE = HIERARCHICAL_QUERY_TEMPLATE.replace("$in", "from").replace("$out", "to"); + @Getter + @Value("${sql.relations.max_level:50}") + int maxLevelAllowed; //This value has to be reasonable small to prevent infinite recursion as early as possible + private final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; private final DefaultQueryLogComponent queryLog; @@ -580,7 +598,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { .append("nr.").append(fromOrTo).append("_type").append(" = re.").append(toOrFrom).append("_type"); notExistsPart.append(")"); - whereFilter += " and ( re.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; + whereFilter += " and ( r_int.lvl = " + entityFilter.getMaxLevel() + " OR " + notExistsPart.toString() + ")"; } from = String.format(from, lvlFilter, whereFilter); String query = "( " + selectFields + from + ")"; @@ -659,7 +677,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { .append(whereFilter.toString().replaceAll("re\\.", "nr\\.")); notExistsPart.append(")"); - whereFilter.append(" and ( re.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); + whereFilter.append(" and ( r_int.lvl = ").append(entityFilter.getMaxLevel()).append(" OR ").append(notExistsPart.toString()).append(")"); } from = String.format(from, lvlFilter, " WHERE " + whereFilter); return "( " + selectFields + from + ")"; @@ -693,8 +711,12 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { return whereFilter.toString(); } - private String getLvlFilter(int maxLevel) { - return maxLevel > 0 ? ("and lvl <= " + (maxLevel - 1)) : ""; + String getLvlFilter(int maxLevel) { + return "and re.lvl <= " + (getMaxLevel(maxLevel) - 1); + } + + int getMaxLevel(int maxLevel) { + return (maxLevel <= 0 || maxLevel > this.maxLevelAllowed) ? this.maxLevelAllowed : maxLevel; } private String getQueryTemplate(EntitySearchDirection direction) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 5e10cabda2..91cc1a0c9f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.relation; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.ConcurrencyFailureException; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; @@ -163,11 +164,16 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple @Override public boolean deleteOutboundRelations(TenantId tenantId, EntityId entity) { - boolean relationExistsBeforeDelete = relationRepository - .findAllByFromIdAndFromType(entity.getId(), entity.getEntityType().name()) - .size() > 0; - if (relationExistsBeforeDelete) { - relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); + boolean relationExistsBeforeDelete = false; + try { + relationExistsBeforeDelete = relationRepository + .findAllByFromIdAndFromType(entity.getId(), entity.getEntityType().name()) + .size() > 0; + if (relationExistsBeforeDelete) { + relationRepository.deleteByFromIdAndFromType(entity.getId(), entity.getEntityType().name()); + } + } catch (ConcurrencyFailureException e) { + log.debug("Concurrency exception while deleting relations [{}]", entity, e); } return relationExistsBeforeDelete; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 040e60daad..483ab39f06 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.model.sql.RuleChainEntity; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import java.util.Collection; import java.util.Objects; import java.util.UUID; @@ -97,8 +98,14 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name) { + return DaoUtil.convertDataList(ruleChainRepository.findByTenantIdAndTypeAndName(tenantId.getId(), type, name)); + } + @Override public Long countByTenantId(TenantId tenantId) { return ruleChainRepository.countByTenantId(tenantId.getId()); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index 86a147cc57..fda0a0d7f0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -23,6 +23,7 @@ import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.model.sql.RuleChainEntity; +import java.util.List; import java.util.UUID; public interface RuleChainRepository extends PagingAndSortingRepository { @@ -55,10 +56,13 @@ public interface RuleChainRepository extends PagingAndSortingRepository findAutoAssignByTenantId(@Param("tenantId") UUID tenantId, - @Param("searchText") String searchText, - Pageable pageable); + @Param("searchText") String searchText, + Pageable pageable); RuleChainEntity findByTenantIdAndTypeAndRootIsTrue(UUID tenantId, RuleChainType ruleChainType); Long countByTenantId(UUID tenantId); + + List findByTenantIdAndTypeAndName(UUID tenantId, RuleChainType type, String name); + } 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 157755db19..d4d86bfb61 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 @@ -31,12 +31,17 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.common.stats.DefaultCounter; +import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; -import org.thingsboard.server.common.msg.tools.TbRateLimits; +import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; @@ -75,22 +80,32 @@ public abstract class AbstractBufferedRateExecutor tenantNamesCache = new HashMap<>(); + + private final boolean printTenantNames; + public AbstractBufferedRateExecutor(int queueLimit, int concurrencyLimit, long maxWaitTime, int dispatcherThreads, int callbackThreads, long pollMs, - boolean perTenantLimitsEnabled, String perTenantLimitsConfiguration, int printQueriesFreq, StatsFactory statsFactory) { + boolean perTenantLimitsEnabled, String perTenantLimitsConfiguration, int printQueriesFreq, StatsFactory statsFactory, + EntityService entityService, boolean printTenantNames) { this.maxWaitTime = maxWaitTime; this.pollMs = pollMs; this.concurrencyLimit = concurrencyLimit; this.printQueriesFreq = printQueriesFreq; this.queue = new LinkedBlockingDeque<>(queueLimit); - this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads, ThingsBoardThreadFactory.forName("nosql-dispatcher")); - this.callbackExecutor = ThingsBoardExecutors.newWorkStealingPool(callbackThreads, getClass()); - this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("nosql-timeout")); + this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads, ThingsBoardThreadFactory.forName("nosql-" + getBufferName() + "-dispatcher")); + this.callbackExecutor = ThingsBoardExecutors.newWorkStealingPool(callbackThreads, "nosql-" + getBufferName() + "-callback"); + this.timeoutExecutor = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("nosql-" + getBufferName() + "-timeout")); this.perTenantLimitsEnabled = perTenantLimitsEnabled; this.perTenantLimitsConfiguration = perTenantLimitsConfiguration; this.stats = new BufferedRateExecutorStats(statsFactory); - String concurrencyLevelKey = StatsType.RATE_EXECUTOR.getName() + "." + CONCURRENCY_LEVEL; + String concurrencyLevelKey = StatsType.RATE_EXECUTOR.getName() + "." + CONCURRENCY_LEVEL + getBufferName(); //metric name may change with buffer name suffix this.concurrencyLevel = statsFactory.createGauge(concurrencyLevelKey, new AtomicInteger(0)); + this.entityService = entityService; + this.printTenantNames = printTenantNames; + for (int i = 0; i < dispatcherThreads; i++) { dispatcherExecutor.submit(this::dispatch); } @@ -144,6 +159,8 @@ public abstract class AbstractBufferedRateExecutor execute(AsyncTaskContext taskCtx); + public abstract String getBufferName(); + private void dispatch() { log.info("Buffered rate executor thread started"); while (!Thread.interrupted()) { @@ -264,4 +281,51 @@ public abstract class AbstractBufferedRateExecutor defaultCounter.get() > 0) + .count(); + + if (queueSize > 0 + || rateLimitedTenantsCount > 0 + || concurrencyLevel.get() > 0 + || stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0) + ) { + StringBuilder statsBuilder = new StringBuilder(); + + statsBuilder.append("queueSize").append(" = [").append(queueSize).append("] "); + stats.getStatsCounters().forEach(counter -> { + statsBuilder.append(counter.getName()).append(" = [").append(counter.get()).append("] "); + }); + statsBuilder.append("totalRateLimitedTenants").append(" = [").append(rateLimitedTenantsCount).append("] "); + statsBuilder.append(CONCURRENCY_LEVEL).append(" = [").append(concurrencyLevel.get()).append("] "); + + stats.getStatsCounters().forEach(StatsCounter::clear); + log.info("Permits {}", statsBuilder); + } + + stats.getRateLimitedTenants().entrySet().stream() + .filter(entry -> entry.getValue().get() > 0) + .forEach(entry -> { + TenantId tenantId = entry.getKey(); + DefaultCounter counter = entry.getValue(); + int rateLimitedRequests = counter.get(); + counter.clear(); + if (printTenantNames) { + String name = tenantNamesCache.computeIfAbsent(tenantId, tId -> { + try { + return entityService.fetchEntityNameAsync(TenantId.SYS_TENANT_ID, tenantId).get(); + } catch (Exception e) { + log.error("[{}] Failed to get tenant name", tenantId, e); + return "N/A"; + } + }); + log.info("[{}][{}] Rate limited requests: {}", tenantId, name, rateLimitedRequests); + } else { + log.info("[{}] Rate limited requests: {}", tenantId, rateLimitedRequests); + } + }); + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlDaoServiceTestSuite.java new file mode 100644 index 0000000000..b89c70e8f4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlDaoServiceTestSuite.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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; + +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; +import org.junit.runner.RunWith; + +@RunWith(ClasspathSuite.class) +@ClassnameFilters({ + "org.thingsboard.server.dao.service.psql.*SqlTest", + "org.thingsboard.server.dao.service.attributes.psql.*SqlTest", + "org.thingsboard.server.dao.service.event.psql.*SqlTest", + "org.thingsboard.server.dao.service.timeseries.psql.*SqlTest" +}) +public class PostgreSqlDaoServiceTestSuite { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java new file mode 100644 index 0000000000..429fa1d711 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2021 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; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.URL; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +@Slf4j +public class PostgreSqlInitializer { + + private static final List sqlFiles = List.of( + "sql/schema-ts-psql.sql", + "sql/schema-entities.sql", + "sql/schema-entities-idx.sql", + "sql/system-data.sql", + "sql/system-test-psql.sql"); + private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql"; + + public static void initDb(Connection conn) { + cleanUpDb(conn); + log.info("initialize Postgres DB..."); + try { + for (String sqlFile : sqlFiles) { + URL sqlFileUrl = Resources.getResource(sqlFile); + String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8); + conn.createStatement().execute(sql); + } + } catch (IOException | SQLException e) { + throw new RuntimeException("Unable to init the Postgres database. Reason: " + e.getMessage(), e); + } + log.info("Postgres DB is initialized!"); + } + + private static void cleanUpDb(Connection conn) { + log.info("clean up Postgres DB..."); + try { + URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile); + String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8); + conn.createStatement().execute(dropAllTablesSql); + } catch (IOException | SQLException e) { + throw new RuntimeException("Unable to clean up the Postgres database. Reason: " + e.getMessage(), e); + } + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java index e8eae60131..ef371569dd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/nosql/CassandraPartitionsCacheTest.java @@ -20,6 +20,7 @@ import com.datastax.oss.driver.api.core.cql.BoundStatement; import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.Statement; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,6 +39,7 @@ import java.util.UUID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willReturn; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -58,9 +60,6 @@ public class CassandraPartitionsCacheTest { @Mock private Environment environment; - @Mock - private CassandraBufferedRateExecutor rateLimiter; - @Mock private CassandraCluster cluster; @@ -74,7 +73,6 @@ public class CassandraPartitionsCacheTest { ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "systemTtl", 0); ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "setNullValuesEnabled", false); ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "environment", environment); - ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "rateLimiter", rateLimiter); ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "cluster", cluster); when(cluster.getDefaultReadConsistencyLevel()).thenReturn(ConsistencyLevel.ONE); @@ -88,7 +86,9 @@ public class CassandraPartitionsCacheTest { when(boundStatement.setUuid(anyInt(), any(UUID.class))).thenReturn(boundStatement); when(boundStatement.setLong(anyInt(), any(Long.class))).thenReturn(boundStatement); - doReturn(Futures.immediateFuture(0)).when(cassandraBaseTimeseriesDao).getFuture(any(TbResultSetFuture.class), any()); + willReturn(new TbResultSetFuture(SettableFuture.create())).given(cassandraBaseTimeseriesDao).executeAsyncWrite(any(), any()); + + doReturn(Futures.immediateFuture(0)).when(cassandraBaseTimeseriesDao).getFuture(any(), any()); } @Test @@ -107,4 +107,5 @@ public class CassandraPartitionsCacheTest { } verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class)); } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java index 2a1ff95062..cfd7923969 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java @@ -47,7 +47,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; -public class BaseDeviceProfileServiceTest extends AbstractServiceTest { +public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private IdComparator deviceProfileInfoIdComparator = new IdComparator<>(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 831d92965b..e58191b60d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -17,14 +17,17 @@ package org.thingsboard.server.dao.service; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -69,6 +72,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; +import org.thingsboard.server.dao.sql.relation.RelationRepository; import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.ArrayList; @@ -82,9 +86,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +@Slf4j public abstract class BaseEntityServiceTest extends AbstractServiceTest { + static final int ENTITY_COUNT = 5; + @Autowired private AttributesService attributesService; @@ -96,6 +104,9 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { @Autowired private JdbcTemplate template; + @Autowired + private RelationRepository relationRepository; + @Before public void before() { Tenant tenant = new Tenant(); @@ -110,7 +121,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); } - + @Test public void testCountEntitiesByQuery() throws InterruptedException { List devices = new ArrayList<>(); @@ -154,12 +165,12 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { Assert.assertEquals(0, count); } - + @Test public void testCountHierarchicalEntitiesByQuery() throws InterruptedException { List assets = new ArrayList<>(); List devices = new ArrayList<>(); - createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); + createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); RelationsQueryFilter filter = new RelationsQueryFilter(); filter.setRootEntity(tenantId); @@ -168,7 +179,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { EntityCountQuery countQuery = new EntityCountQuery(filter); long count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); - Assert.assertEquals(30, count); + Assert.assertEquals(31, count); //due to the loop relations in hierarchy, the TenantId included in total count (1*Tenant + 5*Asset + 5*5*Devices = 31) filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); count = entityService.countEntitiesByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), countQuery); @@ -304,11 +315,25 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { @Test public void testHierarchicalFindEntityDataWithAttributesByQuery() throws ExecutionException, InterruptedException { + doTestHierarchicalFindEntityDataWithAttributesByQuery(0, false); + } + + @Test + public void testHierarchicalFindEntityDataWithAttributesByQueryWithLevel() throws ExecutionException, InterruptedException { + doTestHierarchicalFindEntityDataWithAttributesByQuery(2, false); + } + + @Test + public void testHierarchicalFindEntityDataWithAttributesByQueryWithLastLevelOnly() throws ExecutionException, InterruptedException { + doTestHierarchicalFindEntityDataWithAttributesByQuery(2, true); + } + + private void doTestHierarchicalFindEntityDataWithAttributesByQuery(final int maxLevel, final boolean fetchLastLevelOnly) throws ExecutionException, InterruptedException { List assets = new ArrayList<>(); List devices = new ArrayList<>(); List temperatures = new ArrayList<>(); List highTemperatures = new ArrayList<>(); - createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); + createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { @@ -321,6 +346,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { filter.setRootEntity(tenantId); filter.setDirection(EntitySearchDirection.FROM); filter.setFilters(Collections.singletonList(new RelationEntityTypeFilter("Contains", Collections.singletonList(EntityType.DEVICE)))); + filter.setMaxLevel(maxLevel); + filter.setFetchLastLevelOnly(fetchLastLevelOnly); EntityDataSortOrder sortOrder = new EntityDataSortOrder( new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC @@ -373,14 +400,13 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } - @Test public void testHierarchicalFindDevicesWithAttributesByQuery() throws ExecutionException, InterruptedException { List assets = new ArrayList<>(); List devices = new ArrayList<>(); List temperatures = new ArrayList<>(); List highTemperatures = new ArrayList<>(); - createTestHierarchy(assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); + createTestHierarchy(tenantId, assets, devices, new ArrayList<>(), new ArrayList<>(), temperatures, highTemperatures); List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < devices.size(); i++) { @@ -393,6 +419,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { filter.setRootEntity(tenantId); filter.setDirection(EntitySearchDirection.FROM); filter.setRelationType("Contains"); + filter.setMaxLevel(2); + filter.setFetchLastLevelOnly(true); EntityDataSortOrder sortOrder = new EntityDataSortOrder( new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC @@ -446,14 +474,14 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } - + @Test public void testHierarchicalFindAssetsWithAttributesByQuery() throws ExecutionException, InterruptedException { List assets = new ArrayList<>(); List devices = new ArrayList<>(); List consumptions = new ArrayList<>(); List highConsumptions = new ArrayList<>(); - createTestHierarchy(assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); + createTestHierarchy(tenantId, assets, devices, consumptions, highConsumptions, new ArrayList<>(), new ArrayList<>()); List>> attributeFutures = new ArrayList<>(); for (int i = 0; i < assets.size(); i++) { @@ -518,8 +546,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } - private void createTestHierarchy(List assets, List devices, List consumptions, List highConsumptions, List temperatures, List highTemperatures) throws InterruptedException { - for (int i = 0; i < 5; i++) { + private void createTestHierarchy(TenantId tenantId, List assets, List devices, List consumptions, List highConsumptions, List temperatures, List highTemperatures) throws InterruptedException { + for (int i = 0; i < ENTITY_COUNT; i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); asset.setName("Asset" + i); @@ -529,18 +557,19 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { //TO make sure devices have different created time Thread.sleep(1); assets.add(asset); - EntityRelation er = new EntityRelation(); - er.setFrom(tenantId); - er.setTo(asset.getId()); - er.setType("Manages"); - er.setTypeGroup(RelationTypeGroup.COMMON); - relationService.saveRelation(tenantId, er); + createRelation(tenantId, "Manages", tenantId, asset.getId()); long consumption = (long) (Math.random() * 100); consumptions.add(consumption); if (consumption > 50) { highConsumptions.add(consumption); } - for (int j = 0; j < 5; j++) { + + //tenant -> asset : one-to-one but many edges + for (int n = 0; n < ENTITY_COUNT; n++) { + createRelation(tenantId, "UseCase-" + n, tenantId, asset.getId()); + } + + for (int j = 0; j < ENTITY_COUNT; j++) { Device device = new Device(); device.setTenantId(tenantId); device.setName("A" + i + "Device" + j); @@ -550,22 +579,125 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { //TO make sure devices have different created time Thread.sleep(1); devices.add(device); - er = new EntityRelation(); - er.setFrom(asset.getId()); - er.setTo(device.getId()); - er.setType("Contains"); - er.setTypeGroup(RelationTypeGroup.COMMON); - relationService.saveRelation(tenantId, er); + createRelation(tenantId, "Contains", asset.getId(), device.getId()); long temperature = (long) (Math.random() * 100); temperatures.add(temperature); if (temperature > 45) { highTemperatures.add(temperature); } + + //asset -> device : one-to-one but many edges + for (int n = 0; n < ENTITY_COUNT; n++) { + createRelation(tenantId, "UseCase-" + n, asset.getId(), device.getId()); + } + } + } + + //asset -> device one-to-many shared with other assets + for (int n = 0; n < devices.size(); n = n + ENTITY_COUNT) { + createRelation(tenantId, "SharedWithAsset0", assets.get(0).getId(), devices.get(n).getId()); + } + + createManyCustomRelationsBetweenTwoNodes(tenantId, "UseCase", assets, devices); + createHorizontalRingRelations(tenantId, "Ring(Loop)-Ast", assets); + createLoopRelations(tenantId, "Loop-Tnt-Ast-Dev", tenantId, assets.get(0).getId(), devices.get(0).getId()); + createLoopRelations(tenantId, "Loop-Tnt-Ast", tenantId, assets.get(1).getId()); + createLoopRelations(tenantId, "Loop-Ast-Tnt-Ast", assets.get(2).getId(), tenantId, assets.get(3).getId()); + + //printAllRelations(); + } + + private ResultSetExtractor>> getListResultSetExtractor() { + return rs -> { + List> list = new ArrayList<>(); + final int columnCount = rs.getMetaData().getColumnCount(); + List columns = new ArrayList<>(columnCount); + for (int i = 1; i <= columnCount; i++) { + columns.add(rs.getMetaData().getColumnName(i)); + } + list.add(columns); + while (rs.next()) { + List data = new ArrayList<>(columnCount); + for (int i = 1; i <= columnCount; i++) { + data.add(rs.getString(i)); + } + list.add(data); } + return list; + }; + } + + /* + * This useful to reproduce exact data in the PostgreSQL and play around with pgadmin query and analyze tool + * */ + private void printAllRelations() { + System.out.println("" + + "DO\n" + + "$$\n" + + " DECLARE\n" + + " someint integer;\n" + + " BEGIN\n" + + " DROP TABLE IF EXISTS relation_test;\n" + + " CREATE TABLE IF NOT EXISTS relation_test\n" + + " (\n" + + " from_id uuid,\n" + + " from_type varchar(255),\n" + + " to_id uuid,\n" + + " to_type varchar(255),\n" + + " relation_type_group varchar(255),\n" + + " relation_type varchar(255),\n" + + " additional_info varchar,\n" + + " CONSTRAINT relation_test_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)\n" + + " );"); + + relationRepository.findAll().forEach(r -> + System.out.printf("INSERT INTO relation_test (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" + + " VALUES (%s, %s, %s, %s, %s, %s, %s);\n", + quote(r.getFromId()), quote(r.getFromType()), quote(r.getToId()), quote(r.getToType()), + quote(r.getRelationTypeGroup()), quote(r.getRelationType()), quote(r.getAdditionalInfo())) + ); + + System.out.println("" + + " END\n" + + "$$;"); + } + + private String quote(Object s) { + return s == null ? null : "'" + s + "'"; + } + + void createLoopRelations(TenantId tenantId, String type, EntityId... ids) { + assertThat("ids lenght", ids.length, Matchers.greaterThanOrEqualTo(1)); + //chain all from the head to the tail + for (int i = 1; i < ids.length; i++) { + relationService.saveRelation(tenantId, new EntityRelation(ids[i - 1], ids[i], type, RelationTypeGroup.COMMON)); + } + //chain tail -> head + relationService.saveRelation(tenantId, new EntityRelation(ids[ids.length - 1], ids[0], type, RelationTypeGroup.COMMON)); + } + + void createHorizontalRingRelations(TenantId tenantId, String type, List assets) { + createLoopRelations(tenantId, type, assets.stream().map(Asset::getId).toArray(EntityId[]::new)); + } + + void createManyCustomRelationsBetweenTwoNodes(TenantId tenantId, String type, List assets, List devices) { + for (int i = 1; i <= 5; i++) { + final String typeI = type + i; + createOneToManyRelations(tenantId, typeI, tenantId, assets.stream().map(Asset::getId).collect(Collectors.toList())); + assets.forEach(asset -> + createOneToManyRelations(tenantId, typeI, asset.getId(), devices.stream().map(Device::getId).collect(Collectors.toList()))); } } - + void createOneToManyRelations(TenantId tenantId, String type, EntityId from, List toIds) { + toIds.forEach(toId -> createRelation(tenantId, type, from, toId)); + } + + void createRelation(TenantId tenantId, String type, EntityId from, EntityId toId) { + relationService.saveRelation(tenantId, new EntityRelation(from, toId, type, RelationTypeGroup.COMMON)); + } + + @Test public void testSimpleFindEntityDataByQuery() throws InterruptedException { List devices = new ArrayList<>(); @@ -871,7 +1003,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } @Test - public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException{ + public void testBuildNumericPredicateQueryOperations() throws ExecutionException, InterruptedException { List devices = new ArrayList<>(); List temperatures = new ArrayList<>(); @@ -1031,7 +1163,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { deviceService.deleteDevicesByTenantId(tenantId); } - + @Test public void testFindEntityDataByQueryWithTimeseries() throws ExecutionException, InterruptedException { @@ -1122,7 +1254,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } @Test - public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException{ + public void testBuildStringPredicateQueryOperations() throws ExecutionException, InterruptedException { List devices = new ArrayList<>(); List attributeStrings = new ArrayList<>(); @@ -1142,11 +1274,11 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { devices.add(deviceService.saveDevice(device)); //TO make sure devices have different created time Thread.sleep(1); - List operationValues= Arrays.asList(StringFilterPredicate.StringOperation.values()); + List operationValues = Arrays.asList(StringFilterPredicate.StringOperation.values()); StringFilterPredicate.StringOperation operation = operationValues.get(new Random().nextInt(operationValues.size())); String operationName = operation.name(); attributeStrings.add(operationName); - switch(operation){ + switch (operation) { case EQUAL: equalStrings.add(operationName); notContainsStrings.add(operationName); @@ -1302,7 +1434,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } @Test - public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException{ + public void testBuildStringPredicateQueryOperationsForEntityType() throws ExecutionException, InterruptedException { List devices = new ArrayList<>(); @@ -1419,7 +1551,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { } @Test - public void testBuildSimplePredicateQueryOperations() throws InterruptedException{ + public void testBuildSimplePredicateQueryOperations() throws InterruptedException { List devices = new ArrayList<>(); @@ -1492,7 +1624,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { return loadedEntities; } - private List createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value){ + private List createStringKeyFilters(String key, EntityKeyType keyType, StringFilterPredicate.StringOperation operation, String value) { KeyFilter filter = new KeyFilter(); filter.setKey(new EntityKey(keyType, key)); StringFilterPredicate predicate = new StringFilterPredicate(); @@ -1503,7 +1635,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { return Collections.singletonList(filter); } - private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value){ + private KeyFilter createNumericKeyFilter(String key, EntityKeyType keyType, NumericFilterPredicate.NumericOperation operation, double value) { KeyFilter filter = new KeyFilter(); filter.setKey(new EntityKey(keyType, key)); NumericFilterPredicate predicate = new NumericFilterPredicate(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java index c3073434e0..743591863a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java @@ -31,7 +31,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import java.util.Arrays; import java.util.UUID; -public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest { +public abstract class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest { @Autowired protected OAuth2ConfigTemplateService oAuth2ConfigTemplateService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index 4077bb2176..0ad3470c99 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -43,7 +43,7 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; -public class BaseOAuth2ServiceTest extends AbstractServiceTest { +public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { private static final OAuth2Info EMPTY_PARAMS = new OAuth2Info(false, Collections.emptyList()); @Autowired diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java index 2ebb955c61..d77143f0a0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java @@ -34,7 +34,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -public class BaseTenantProfileServiceTest extends AbstractServiceTest { +public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private IdComparator tenantProfileInfoIdComparator = new IdComparator<>(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DaoPostgreSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DaoPostgreSqlTest.java new file mode 100644 index 0000000000..43ed1f4d02 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DaoPostgreSqlTest.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 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; + +import org.springframework.test.context.TestPropertySource; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:psql-test.properties"}) +public @interface DaoPostgreSqlTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/psql/EntityServicePostgreSqlTest.java similarity index 77% rename from dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java rename to dao/src/test/java/org/thingsboard/server/dao/service/psql/EntityServicePostgreSqlTest.java index 0c59ae49c8..5b8c6d4aca 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/sql/EntityServiceSqlTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/psql/EntityServicePostgreSqlTest.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.service.sql; +package org.thingsboard.server.dao.service.psql; import org.thingsboard.server.dao.service.BaseEntityServiceTest; -import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.service.DaoPostgreSqlTest; -@DaoSqlTest -public class EntityServiceSqlTest extends BaseEntityServiceTest { +@DaoPostgreSqlTest +public class EntityServicePostgreSqlTest extends BaseEntityServiceTest { } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepositoryTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepositoryTest.java new file mode 100644 index 0000000000..1a0ce4cebc --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepositoryTest.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2021 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.query; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.support.TransactionTemplate; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = DefaultEntityQueryRepository.class) +public class DefaultEntityQueryRepositoryTest { + + @MockBean + NamedParameterJdbcTemplate jdbcTemplate; + @MockBean + TransactionTemplate transactionTemplate; + @MockBean + DefaultQueryLogComponent queryLog; + + @Autowired + DefaultEntityQueryRepository repo; + + /* + * This value has to be reasonable small to prevent infinite recursion as early as possible + * */ + @Test + public void givenDefaultMaxLevel_whenStaticConstant_thenEqualsTo() { + assertThat(repo.getMaxLevelAllowed(), equalTo(50)); + } + + @Test + public void givenMaxLevelZeroOrNegative_whenGetMaxLevel_thenReturnDefaultMaxLevel() { + assertThat(repo.getMaxLevel(0), equalTo(repo.getMaxLevelAllowed())); + assertThat(repo.getMaxLevel(-1), equalTo(repo.getMaxLevelAllowed())); + assertThat(repo.getMaxLevel(-2), equalTo(repo.getMaxLevelAllowed())); + assertThat(repo.getMaxLevel(Integer.MIN_VALUE), equalTo(repo.getMaxLevelAllowed())); + } + + @Test + public void givenMaxLevelPositive_whenGetMaxLevel_thenValueTheSame() { + assertThat(repo.getMaxLevel(1), equalTo(1)); + assertThat(repo.getMaxLevel(2), equalTo(2)); + assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed()), equalTo(repo.getMaxLevelAllowed())); + assertThat(repo.getMaxLevel(repo.getMaxLevelAllowed() + 1), equalTo(repo.getMaxLevelAllowed())); + assertThat(repo.getMaxLevel(Integer.MAX_VALUE), equalTo(repo.getMaxLevelAllowed())); + } + +} diff --git a/dao/src/test/resources/psql-test.properties b/dao/src/test/resources/psql-test.properties new file mode 100644 index 0000000000..fb65966acf --- /dev/null +++ b/dao/src/test/resources/psql-test.properties @@ -0,0 +1,47 @@ +database.ts.type=sql +database.ts_latest.type=sql +sql.ts_inserts_executor_type=fixed +sql.ts_inserts_fixed_thread_pool_size=200 +sql.ts_key_value_partitioning=MONTHS +# +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +spring.jpa.properties.hibernate.order_by.default_null_ordering=last +spring.jpa.properties.hibernate.jdbc.log.warnings=false +spring.jpa.show-sql=false +spring.jpa.hibernate.ddl-auto=none +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.url=jdbc:tc:postgresql:12.8:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.PostgreSqlInitializer::initDb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +#org.postgresql.Driver +spring.datasource.hikari.maximumPoolSize=50 +service.type=monolith +#database.ts.type=timescale +#database.ts.type=sql +#database.entities.type=sql +# +#sql.ts_inserts_executor_type=fixed +#sql.ts_inserts_fixed_thread_pool_size=200 +#sql.ts_key_value_partitioning=MONTHS +# +#spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +#spring.jpa.show-sql=false +#spring.jpa.hibernate.ddl-auto=none +#spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +# +#spring.datasource.username=postgres +#spring.datasource.password=postgres +#spring.datasource.url=jdbc:postgresql://localhost:5432/sqltest +#spring.datasource.driverClassName=org.postgresql.Driver +#spring.datasource.hikari.maximumPoolSize = 50 +queue.core.pack-processing-timeout=3000 +queue.rule-engine.pack-processing-timeout=3000 +queue.rule-engine.queues[0].name=Main +queue.rule-engine.queues[0].topic=tb_rule_engine.main +queue.rule-engine.queues[0].poll-interval=25 +queue.rule-engine.queues[0].partitions=3 +queue.rule-engine.queues[0].pack-processing-timeout=3000 +queue.rule-engine.queues[0].processing-strategy.type=SKIP_ALL_FAILURES +queue.rule-engine.queues[0].submit-strategy.type=BURST +sql.log_entity_queries=true diff --git a/dao/src/test/resources/sql/system-test-psql.sql b/dao/src/test/resources/sql/system-test-psql.sql new file mode 100644 index 0000000000..16dcb8c3ae --- /dev/null +++ b/dao/src/test/resources/sql/system-test-psql.sql @@ -0,0 +1,2 @@ +--PostgreSQL specific truncate to fit constraints +TRUNCATE TABLE device_credentials, device, device_profile, rule_node_state, rule_node, rule_chain; \ No newline at end of file diff --git a/k8s/common/thingsboard.yml b/k8s/common/thingsboard.yml index 51f39823df..ee360c27b7 100644 --- a/k8s/common/thingsboard.yml +++ b/k8s/common/thingsboard.yml @@ -424,6 +424,10 @@ spec: backend: serviceName: tb-node servicePort: 8080 + - path: /assets/help/.*/rulenode/.* + backend: + serviceName: tb-node + servicePort: 8080 - path: /oauth2/.* backend: serviceName: tb-node diff --git a/pom.xml b/pom.xml index 4524143085..4a228fba35 100755 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,7 @@ 3.0.2 2.6.1 1.0.0 + 1.5.10 0.7 1.15.0 1.67 @@ -1623,6 +1624,11 @@ springfox-swagger2 ${springfox-swagger.version} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + org.bouncycastle bcprov-jdk15on @@ -1637,6 +1643,17 @@ org.hsqldb hsqldb ${hsqldb.version} + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + org.testcontainers + jdbc + ${testcontainers.version} test 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 30dc57eeb7..a64dd97cdc 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 @@ -57,6 +57,7 @@ import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.Event; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; @@ -1131,11 +1132,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { public Optional saveDeviceWithCredentials(Device device, DeviceCredentials credentials) { try { - Map, Object> deviceCredentials = new ConcurrentHashMap<>(); - deviceCredentials.put(Device.class, device); - deviceCredentials.put(DeviceCredentials.class, credentials); -// return restTemplate.postForEntity(baseURL + "/api/lwm2m/device-credentials", deviceCredentials, Device.class).getBody(); - ResponseEntity deviceOpt = restTemplate.postForEntity(baseURL + "/api/lwm2m/device-credentials", deviceCredentials, Device.class); + SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(device, credentials); + ResponseEntity deviceOpt = restTemplate.postForEntity(baseURL + "/api/device-with-credentials", request, Device.class); return Optional.ofNullable(deviceOpt.getBody()); } catch (HttpClientErrorException exception) { if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { @@ -1146,7 +1144,6 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { } } - public PageData getTenantDevices(String type, PageLink pageLink) { Map params = new HashMap<>(); params.put("type", type); 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 3474ee2c9d..239c86ed57 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 @@ -245,7 +245,9 @@ public interface TbContext { CassandraCluster getCassandraCluster(); - TbResultSetFuture submitCassandraTask(CassandraStatementTask task); + TbResultSetFuture submitCassandraReadTask(CassandraStatementTask task); + + TbResultSetFuture submitCassandraWriteTask(CassandraStatementTask task); PageData findRuleNodeStates(PageLink pageLink); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index f44c2a48d6..a50b4bbc26 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -39,12 +39,8 @@ public class TbNodeUtils { private static final ObjectMapper mapper = new ObjectMapper(); - private static final String METADATA_VARIABLE_TEMPLATE = "${%s}"; - private static final Pattern DATA_PATTERN = Pattern.compile("(\\$\\[)(.*?)(])"); - private static final String DATA_VARIABLE_TEMPLATE = "$[%s]"; - public static T convert(TbNodeConfiguration configuration, Class clazz) throws TbNodeException { try { return mapper.treeToValue(configuration.getData(), clazz); @@ -80,7 +76,7 @@ public class TbNodeUtils { } if (jsonNode != null && jsonNode.isValueNode()) { - result = result.replace(String.format(DATA_VARIABLE_TEMPLATE, group), jsonNode.asText()); + result = result.replace(formatDataVarTemplate(group), jsonNode.asText()); } } } @@ -106,8 +102,14 @@ public class TbNodeUtils { } private static String processVar(String pattern, String key, String val) { - String varPattern = String.format(METADATA_VARIABLE_TEMPLATE, key); - return pattern.replace(varPattern, val); + return pattern.replace(formatMetadataVarTemplate(key), val); } + static String formatDataVarTemplate(String key) { + return "$[" + key + ']'; + } + + static String formatMetadataVarTemplate(String key) { + return "${" + key + '}'; + } } 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 11687e0415..30e9eb12b5 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 @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.api.util; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -25,9 +26,15 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.common.util.JacksonUtil; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + @RunWith(MockitoJUnitRunner.class) public class TbNodeUtilsTest { + private static final String DATA_VARIABLE_TEMPLATE = "$[%s]"; + private static final String METADATA_VARIABLE_TEMPLATE = "${%s}"; + @Test public void testSimpleReplacement() { String pattern = "ABC ${metadata_key} $[data_key]"; @@ -112,4 +119,27 @@ public class TbNodeUtilsTest { Assert.assertEquals("ABC metadata_value $[key1.key2[0].key3]", result); } + @Test + public void givenKey_whenFormatDataVarTemplate_thenReturnTheSameStringAsFormat() { + assertThat(TbNodeUtils.formatDataVarTemplate("key"), is("$[key]")); + assertThat(TbNodeUtils.formatDataVarTemplate("key"), is(String.format(DATA_VARIABLE_TEMPLATE, "key"))); + + assertThat(TbNodeUtils.formatDataVarTemplate(""), is("$[]")); + assertThat(TbNodeUtils.formatDataVarTemplate(""), is(String.format(DATA_VARIABLE_TEMPLATE, ""))); + + assertThat(TbNodeUtils.formatDataVarTemplate(null), is("$[null]")); + assertThat(TbNodeUtils.formatDataVarTemplate(null), is(String.format(DATA_VARIABLE_TEMPLATE, (String) null))); + } + + @Test + public void givenKey_whenFormatMetadataVarTemplate_thenReturnTheSameStringAsFormat() { + assertThat(TbNodeUtils.formatMetadataVarTemplate("key"), is("${key}")); + assertThat(TbNodeUtils.formatMetadataVarTemplate("key"), is(String.format(METADATA_VARIABLE_TEMPLATE, "key"))); + + assertThat(TbNodeUtils.formatMetadataVarTemplate(""), is("${}")); + assertThat(TbNodeUtils.formatMetadataVarTemplate(""), is(String.format(METADATA_VARIABLE_TEMPLATE, ""))); + + assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is("${null}")); + assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is(String.format(METADATA_VARIABLE_TEMPLATE, (String) null))); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java index 69ad95cf40..a40089b0be 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java @@ -20,6 +20,18 @@ import lombok.Data; @Data public abstract class TbAbstractAlarmNodeConfiguration { + static final String ALARM_DETAILS_BUILD_JS_TEMPLATE = "" + + "var details = {};\n" + + "if (metadata.prevAlarmDetails) {\n" + + " details = JSON.parse(metadata.prevAlarmDetails);\n" + + " //remove prevAlarmDetails from metadata\n" + + " delete metadata.prevAlarmDetails;\n" + + " //now metadata is the same as it comes IN this rule node\n" + + "}\n" + + "\n" + + "\n" + + "return details;"; + private String alarmType; private String alarmDetailsBuildJs; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java index 3de9ca96c7..df29ad072d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java @@ -25,11 +25,7 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat @Override public TbClearAlarmNodeConfiguration defaultConfiguration() { TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration(); - configuration.setAlarmDetailsBuildJs("var details = {};\n" + - "if (metadata.prevAlarmDetails) {\n" + - " details = JSON.parse(metadata.prevAlarmDetails);\n" + - "}\n" + - "return details;"); + configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE); configuration.setAlarmType("General Alarm"); return configuration; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java index 3173b4ab67..2d03612058 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java @@ -35,11 +35,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura @Override public TbCreateAlarmNodeConfiguration defaultConfiguration() { TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration(); - configuration.setAlarmDetailsBuildJs("var details = {};\n" + - "if (metadata.prevAlarmDetails) {\n" + - " details = JSON.parse(metadata.prevAlarmDetails);\n" + - "}\n" + - "return details;"); + configuration.setAlarmDetailsBuildJs(ALARM_DETAILS_BUILD_JS_TEMPLATE); configuration.setAlarmType("General Alarm"); configuration.setSeverity(AlarmSeverity.CRITICAL.name()); configuration.setPropagate(false); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index 5cf1a3a7a5..a56cb644be 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -213,7 +213,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (statement.getConsistencyLevel() == null) { statement.setConsistencyLevel(level); } - return ctx.submitCassandraTask(new CassandraStatementTask(ctx.getTenantId(), getSession(), statement)); + return ctx.submitCassandraWriteTask(new CassandraStatementTask(ctx.getTenantId(), getSession(), statement)); } private static String statementToString(Statement statement) { 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 4a3113baca..989a080f88 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 @@ -38,10 +38,13 @@ import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; @Slf4j @RuleNode( type = ComponentType.ACTION, - name = "delay", + name = "delay (deprecated)", configClazz = TbMsgDelayNodeConfiguration.class, - nodeDescription = "Delays incoming message", - nodeDetails = "Delays messages for configurable period. Please note, this node acknowledges the message from the current queue (message will be removed from queue)", + nodeDescription = "Delays incoming message (deprecated)", + nodeDetails = "Delays messages for a configurable period. " + + "Please note, this node acknowledges the message from the current queue (message will be removed from queue). " + + "Deprecated because the acknowledged message still stays in memory (to be delayed) and this " + + "does not guarantee that message will be processed even if the \"retry failures and timeouts\" processing strategy will be chosen.", icon = "pause", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeMsgDelayConfig" 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 df273dc8f8..49f41762ce 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 @@ -75,8 +75,8 @@ public class TbKafkaNode implements TbNode { Properties properties = new Properties(); properties.put(ProducerConfig.CLIENT_ID_CONFIG, "producer-tb-kafka-node-" + ctx.getSelfId().getId().toString() + "-" + ctx.getServiceId()); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.getBootstrapServers()); - properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, getKafkaSerializerClass(config.getValueSerializer())); - properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, getKafkaSerializerClass(config.getKeySerializer())); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, config.getValueSerializer()); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, config.getKeySerializer()); properties.put(ProducerConfig.ACKS_CONFIG, config.getAcks()); properties.put(ProducerConfig.RETRIES_CONFIG, config.getRetries()); properties.put(ProducerConfig.BATCH_SIZE_CONFIG, config.getBatchSize()); @@ -88,28 +88,12 @@ public class TbKafkaNode implements TbNode { addMetadataKeyValuesAsKafkaHeaders = BooleanUtils.toBooleanDefaultIfNull(config.isAddMetadataKeyValuesAsKafkaHeaders(), false); toBytesCharset = config.getKafkaHeadersCharset() != null ? Charset.forName(config.getKafkaHeadersCharset()) : StandardCharsets.UTF_8; try { - // Ugly workaround to fix org.apache.kafka.common.KafkaException: javax.security.auth.login.LoginException: unable to find LoginModule class - // details: https://stackoverflow.com/questions/57574901/kafka-java-client-classloader-doesnt-find-sasl-scram-login-class - Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader()); this.producer = new KafkaProducer<>(properties); } catch (Exception e) { throw new TbNodeException(e); } } - private Class getKafkaSerializerClass(String serializerClassName) { - Class serializerClass = null; - if (!StringUtils.isEmpty(serializerClassName)) { - try { - serializerClass = Class.forName(serializerClassName); - } catch (ClassNotFoundException e) {} - } - if (serializerClass == null) { - serializerClass = StringSerializer.class; - } - return serializerClass; - } - @Override public void onMsg(TbContext ctx, TbMsg msg) { String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg); 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 3c3fa8e21a..03e0135715 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 @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.metadata; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,6 +34,7 @@ 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.kv.Aggregation; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -50,7 +50,6 @@ 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; -import static org.thingsboard.server.common.data.kv.Aggregation.NONE; /** * Created by mshvayka on 04.09.18. @@ -64,6 +63,7 @@ import static org.thingsboard.server.common.data.kv.Aggregation.NONE; "If selected fetch mode ALL Telemetry will be added like array into Message Metadata where key is Timestamp and value is value of Telemetry.
" + "If selected fetch mode FIRST or LAST Telemetry will be added like string without Timestamp.
" + "Also, the rule node allows you to select telemetry sampling order: ASC or DESC.
" + + "Aggregation feature allows you to fetch aggregated telemetry as a single value by AVG, COUNT, SUM, MIN, MAX, NONE.
" + "Note: The maximum size of the fetched array is 1000 records.\n ", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase") @@ -78,6 +78,7 @@ public class TbGetTelemetryNode implements TbNode { private ObjectMapper mapper; private String fetchMode; private String orderByFetchAll; + private Aggregation aggregation; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { @@ -89,11 +90,20 @@ public class TbGetTelemetryNode implements TbNode { if (StringUtils.isEmpty(orderByFetchAll)) { orderByFetchAll = ASC_ORDER; } + aggregation = parseAggregationConfig(config.getAggregation()); + mapper = new ObjectMapper(); mapper.configure(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature(), false); mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); } + Aggregation parseAggregationConfig(String aggName) { + if (StringUtils.isEmpty(aggName)) { + return Aggregation.NONE; + } + return Aggregation.valueOf(aggName); + } + @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { if (tsKeyNames.isEmpty()) { @@ -120,8 +130,14 @@ public class TbGetTelemetryNode implements TbNode { } private List buildQueries(TbMsg msg, List keys) { + final Interval interval = getInterval(msg); + final long aggIntervalStep = Aggregation.NONE.equals(aggregation) ? 1 : + // exact how it validates on BaseTimeseriesService.validate() + // see CassandraBaseTimeseriesDao.findAllAsync() + interval.getEndTs() - interval.getStartTs(); + return keys.stream() - .map(key -> new BaseReadTsKvQuery(key, getInterval(msg).getStartTs(), getInterval(msg).getEndTs(), 1, limit, NONE, getOrderBy())) + .map(key -> new BaseReadTsKvQuery(key, interval.getStartTs(), interval.getEndTs(), aggIntervalStep, limit, aggregation, getOrderBy())) .collect(Collectors.toList()); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java index e4e673a2ab..1233191af5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.metadata; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.kv.Aggregation; import java.util.Collections; import java.util.List; @@ -46,6 +47,7 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration latestTsKeyNames; @@ -63,6 +65,7 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration>> pendingFutures; - TbHttpClient(TbRestApiCallNodeConfiguration config) throws TbNodeException { + TbHttpClient(TbRestApiCallNodeConfiguration config, EventLoopGroup eventLoopGroupShared) throws TbNodeException { try { this.config = config; if (config.getMaxParallelRequestsCount() > 0) { @@ -139,8 +139,7 @@ public class TbHttpClient { } httpClient = new AsyncRestTemplate(); } else { - this.eventLoopGroup = new NioEventLoopGroup(); - Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(this.eventLoopGroup); + Netty4ClientHttpRequestFactory nettyFactory = new Netty4ClientHttpRequestFactory(getSharedOrCreateEventLoopGroup(eventLoopGroupShared)); nettyFactory.setSslContext(config.getCredentials().initSslContext()); nettyFactory.setReadTimeout(config.getReadTimeoutMs()); httpClient = new AsyncRestTemplate(nettyFactory); @@ -150,6 +149,13 @@ public class TbHttpClient { } } + EventLoopGroup getSharedOrCreateEventLoopGroup(EventLoopGroup eventLoopGroupShared) { + if (eventLoopGroupShared != null) { + return eventLoopGroupShared; + } + return this.eventLoopGroup = new NioEventLoopGroup(); + } + private void checkSystemProxyProperties() throws TbNodeException { boolean useHttpProxy = !StringUtils.isEmpty(System.getProperty("http.proxyHost")) && !StringUtils.isEmpty(System.getProperty("http.proxyPort")); boolean useHttpsProxy = !StringUtils.isEmpty(System.getProperty("https.proxyHost")) && !StringUtils.isEmpty(System.getProperty("https.proxyPort")); 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 2ace27ef22..a23dbf820e 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 @@ -51,7 +51,7 @@ public class TbRestApiCallNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { TbRestApiCallNodeConfiguration config = TbNodeUtils.convert(configuration, TbRestApiCallNodeConfiguration.class); - httpClient = new TbHttpClient(config); + httpClient = new TbHttpClient(config, ctx.getSharedEventLoop()); useRedisQueueForMsgPersistence = config.isUseRedisQueueForMsgPersistence(); if (useRedisQueueForMsgPersistence) { log.warn("[{}][{}] Usage of Redis Template is deprecated starting 2.5 and will have no affect", ctx.getTenantId(), ctx.getSelfId()); 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 1d0b727e5e..196dd1feb7 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 @@ -95,15 +95,8 @@ public class TbSendRPCRequestNode implements TbNode { tmp = msg.getMetaData().getValue(DataConstants.RETRIES); Integer retries = !StringUtils.isEmpty(tmp) ? Integer.parseInt(tmp) : null; - String params; - JsonElement paramsEl = json.get("params"); - if (paramsEl.isJsonPrimitive()) { - params = paramsEl.getAsString(); - } else { - params = gson.toJson(paramsEl); - } - - String additionalInfo = gson.toJson(json.get(DataConstants.ADDITIONAL_INFO)); + String params = parseJsonData(json.get("params")); + String additionalInfo = parseJsonData(json.get(DataConstants.ADDITIONAL_INFO)); RuleEngineDeviceRpcRequest request = RuleEngineDeviceRpcRequest.builder() .oneway(oneway) @@ -144,4 +137,8 @@ public class TbSendRPCRequestNode implements TbNode { return gson.toJson(json); } + private String parseJsonData(JsonElement paramsEl) { + return paramsEl.isJsonPrimitive() ? paramsEl.getAsString() : gson.toJson(paramsEl); + } + } 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 3343ee8cac..17f2ee6af1 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 @@ -12,5 +12,5 @@ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */var y=function(e,t){return(y=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function b(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}y(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function h(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function C(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function v(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var F,x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-node-empty-config",template:"
"}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\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'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],i}return b(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){var t=this;this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((function(e){e?t.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):t.createAlarmConfigForm.get("severity").patchValue(t.alarmSeverities[0],{emitEvent:!1})}))},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.use-dynamically-change-the-severity-of-alar\' | translate }}\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.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\' | translate }}\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \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
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(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,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),P=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId).subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(F||(F={}));var M,w=new Map([[F.CUSTOMER,"tb.rulenode.originator-customer"],[F.TENANT,"tb.rulenode.originator-tenant"],[F.RELATED,"tb.rulenode.originator-related"],[F.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,D=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,K=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var B,H=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(B||(B={}));var G,j,U,z=new Map([[B.TITLE,"tb.rulenode.entity-details-title"],[B.COUNTRY,"tb.rulenode.entity-details-country"],[B.STATE,"tb.rulenode.entity-details-state"],[B.ZIP,"tb.rulenode.entity-details-zip"],[B.ADDRESS,"tb.rulenode.entity-details-address"],[B.ADDRESS2,"tb.rulenode.entity-details-address2"],[B.PHONE,"tb.rulenode.entity-details-phone"],[B.EMAIL,"tb.rulenode.entity-details-email"],[B.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(G||(G={})),function(e){e.ASC="ASC",e.DESC="DESC"}(j||(j={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(U||(U={}));var Q,_=new Map([[U.STANDARD,"tb.rulenode.sqs-queue-standard"],[U.FIFO,"tb.rulenode.sqs-queue-fifo"]]),$=["anonymous","basic","cert.PEM"],W=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),J=["sas","cert.PEM"],Y=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(Q||(Q={}));var Z=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],X=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"]]),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=H,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType: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,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.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([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.prototype.prepareOutputConfig=function(e){return e.tableName=e.tableName.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return b(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=v(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",String)],r.prototype,"requiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyRequiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valRequiredText",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=h([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],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:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),C("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-device-relations-query-config",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-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-relations-query-config",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',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),le=function(e){function r(t,r,n,o){var i,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.truncate=n,s.fb=o,s.placeholder="tb.rulenode.message-type",s.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],s.messageTypes=[],s.messageTypesList=[],s.searchText="",s.propagateChange=function(e){},s.messageTypeConfigForm=s.fb.group({messageType:[null]});try{for(var u=v(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;s.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return s}var l;return b(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",String)],r.prototype,"label",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"placeholder",void 0),h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.ViewChild("chipList",{static:!1}),C("design:type",d.MatChipList)],r.prototype,"chipList",void 0),h([t.ViewChild("messageTypeAutocomplete",{static:!1}),C("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),h([t.ViewChild("messageTypeInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=h([t.Component({selector:"tb-message-types-config",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 {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\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',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),C("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.subscriptions=[],n.disableCertPemCredentials=!1,n.passwordFieldRquired=!0,n.allCredentialsTypes=$,n.credentialsTypeTranslationsMap=W,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.credentialsConfigFormGroup=this.fb.group({type:[null,[i.Validators.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(f.distinctUntilChanged()).subscribe((function(){e.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((function(){e.credentialsTypeChanged()})))},r.prototype.ngOnChanges=function(e){var t,r,n=this;try{for(var a=v(Object.keys(e)),o=a.next();!o.done;o=a.next()){var i=o.value,l=e[i];if(!l.firstChange&&l.currentValue!==l.previousValue)if(l.currentValue&&"disableCertPemCredentials"===i)"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((function(){n.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}catch(e){t={error:e}}finally{try{o&&!o.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}},r.prototype.ngOnDestroy=function(){this.subscriptions.forEach((function(e){return e.unsubscribe()}))},r.prototype.writeValue=function(e){s.isDefinedAndNotNull(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))},r.prototype.setDisabledState=function(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())},r.prototype.updateView=function(){var e=this.credentialsConfigFormGroup.value,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)},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.validate=function(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}},r.prototype.credentialsTypeChanged=function(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()},r.prototype.updateValidators=function(e){void 0===e&&(e=!1);var 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([i.Validators.required]),this.credentialsConfigFormGroup.get("password").setValidators(this.passwordFieldRquired?[i.Validators.required]:[]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(i.Validators.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})},r.prototype.requiredFilesSelected=function(e,t){return void 0===t&&(t=null),function(r){return t||(t=[Object.keys(r.controls)]),(null==r?void 0:r.controls)&&t.some((function(t){return t.every((function(t){return!e(r.controls[t])}))}))?null:{notAllRequiredFilesSelected:!0}}},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",Object)],r.prototype,"disableCertPemCredentials",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"passwordFieldRquired",void 0),r=n=h([t.Component({selector:"tb-credentials-config",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',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),me=function(){function e(e){this.sanitizer=e}return e.prototype.transform=function(e){return this.sanitizer.bypassSecurityTrustHtml(e)},e.ctorParameters=function(){return[{type:g.DomSanitizer}]},e=h([t.Pipe({name:"safeHtml"}),C("design:paramtypes",[g.DomSanitizer])],e)}(),ue=function(){function e(){}return e=h([t.NgModule({declarations:[ae,oe,ie,le,se,me],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule],exports:[ae,oe,ie,le,se,me]})],e)}(),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=U,n.sqsQueueTypes=Object.keys(U),n.sqsQueueTypeTranslationsMap=_,n}return b(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Z,n.ToByteStandartCharsetTypeTranslationMap=X,n}return b(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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.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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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 \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return b(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(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,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.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,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(Q),n}return b(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],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,[i.Validators.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,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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 \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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return b(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(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,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=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([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return b(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=J,n.azureIotHubCredentialsTypeTranslationsMap=Y,n}return b(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.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,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.sendSmsConfigForm},r.prototype.onConfigurationSet=function(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[i.Validators.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[i.Validators.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmsSettings"]},r.prototype.updateValidators=function(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([i.Validators.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.pushToEdgeConfigForm},r.prototype.onConfigurationSet=function(e){this.pushToEdgeConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.pushToCloudConfigForm},r.prototype.onConfigurationSet=function(e){this.pushToCloudConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(){function e(){}return e=h([t.NgModule({declarations:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te,qe,Se],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule,ue],exports:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te,qe,Se]})],e)}(),ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\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}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=H,n}return b(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType: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,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.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([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return b(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Pe=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Me=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=v(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("alarmStatusInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=h([t.Component({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'}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(){function e(){}return e=h([t.NgModule({declarations:[ke,Ne,Ve,Ee,Ae,Le,Pe,Me],imports:[r.CommonModule,a.SharedModule,ue],exports:[ke,Ne,Ve,Ee,Ae,Le,Pe,Me]})],e)}(),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=z,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=v(Object.keys(B)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(B[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(z.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(z.get(B[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("detailsInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=h([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),He=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n.fetchMode=G,n.fetchModes=Object.keys(G),n.samplingOrders=Object.keys(j),n.timeUnits=Object.values(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===G.ALL?(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([])),this.getTelemetryFromDatabaseConfigForm.get("orderBy").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("limit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\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.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\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.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),je=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.calculateDeltaConfigForm},r.prototype.onConfigurationSet=function(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[i.Validators.required]],outputValueKey:[e?e.outputValueKey:null,[i.Validators.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[i.Validators.min(0),i.Validators.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})},r.prototype.updateValidators=function(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([i.Validators.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})},r.prototype.validatorTriggers=function(){return["addPeriodBetweenMsgs"]},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-calculate-delta-config",template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ze=function(){function e(){}return e=h([t.NgModule({declarations:[Re,De,Oe,Ke,Be,He,Ge,je,Ue],imports:[r.CommonModule,a.SharedModule,ue],exports:[Re,De,Oe,Ke,Be,He,Ge,je,Ue]})],e)}(),Qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=F,n.originatorSources=Object.keys(F),n.originatorSourceTranslationMap=w,n}return b(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===F.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),_e=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId).subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),$e=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.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"}],n}return b(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){var t=this;this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],mailBodyType:[e?e.mailBodyType:null],isHtmlTemplate:[e?e.isHtmlTemplate:null],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]}),this.toEmailConfigForm.get("mailBodyType").valueChanges.pipe(f.startWith([null==e?void 0:e.subjectTemplate])).subscribe((function(e){"dynamic"===e?(t.toEmailConfigForm.get("isHtmlTemplate").patchValue("",{emitEvent:!1}),t.toEmailConfigForm.get("isHtmlTemplate").setValidators(i.Validators.required)):t.toEmailConfigForm.get("isHtmlTemplate").clearValidators(),t.toEmailConfigForm.get("isHtmlTemplate").updateValueAndValidity()}))},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),We=function(){function e(){}return e=h([t.NgModule({declarations:[Qe,_e,$e],imports:[r.CommonModule,a.SharedModule,ue],exports:[Qe,_e,$e]})],e)}(),Je=function(){function e(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","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.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","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","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","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","timeseries-key":"Timeseries key","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","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","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","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","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-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",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","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","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","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-interval-patterns":"Use interval patterns","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","use-dynamically-change-the-severity-of-alar":"Use dynamically change the severity of alarm","check-all-keys":"Check that all selected keys 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-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern 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":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","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","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","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":"You should 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-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 Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","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.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-key-required":"Period value key is required.","general-pattern-hint":'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.)'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"},"mail-body-type":{"plain-text":"Plain Text",html:"HTML",dynamic:"Dynamic"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=h([t.NgModule({declarations:[x],imports:[r.CommonModule,a.SharedModule],exports:[Ie,we,ze,We,x]}),C("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=Je,e.ɵa=x,e.ɵb=Ie,e.ɵba=ve,e.ɵbb=Fe,e.ɵbc=xe,e.ɵbd=Te,e.ɵbe=qe,e.ɵbf=Se,e.ɵbg=ue,e.ɵbh=ae,e.ɵbi=oe,e.ɵbj=ie,e.ɵbk=le,e.ɵbl=se,e.ɵbm=me,e.ɵbn=we,e.ɵbo=ke,e.ɵbp=Ne,e.ɵbq=Ve,e.ɵbr=Ee,e.ɵbs=Ae,e.ɵbt=Le,e.ɵbu=Pe,e.ɵbv=Me,e.ɵbw=ze,e.ɵbx=Re,e.ɵby=De,e.ɵbz=Oe,e.ɵc=T,e.ɵca=Ke,e.ɵcb=Be,e.ɵcc=He,e.ɵcd=Ge,e.ɵce=je,e.ɵcf=Ue,e.ɵcg=We,e.ɵch=Qe,e.ɵci=_e,e.ɵcj=$e,e.ɵd=q,e.ɵe=S,e.ɵf=I,e.ɵg=k,e.ɵh=N,e.ɵi=V,e.ɵj=E,e.ɵk=A,e.ɵl=L,e.ɵm=P,e.ɵn=ee,e.ɵo=te,e.ɵp=re,e.ɵq=ne,e.ɵr=de,e.ɵs=pe,e.ɵt=ce,e.ɵu=fe,e.ɵv=ge,e.ɵw=ye,e.ɵx=be,e.ɵy=he,e.ɵz=Ce,Object.defineProperty(e,"__esModule",{value:!0})})); + ***************************************************************************** */var y=function(e,t){return(y=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(e[r]=t[r])})(e,t)};function b(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function r(){this.constructor=e}y(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function h(e,t,r,n){var a,o=arguments.length,i=o<3?t:null===n?n=Object.getOwnPropertyDescriptor(t,r):n;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)i=Reflect.decorate(e,t,r,n);else for(var l=e.length-1;l>=0;l--)(a=e[l])&&(i=(o<3?a(i):o>3?a(t,r,i):a(t,r))||i);return o>3&&i&&Object.defineProperty(t,r,i),i}function C(e,t){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(e,t)}Object.create;function v(e){var t="function"==typeof Symbol&&Symbol.iterator,r=t&&e[t],n=0;if(r)return r.call(e);if(e&&"number"==typeof e.length)return{next:function(){return e&&n>=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}Object.create;var F,x=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.emptyConfigForm},r.prototype.onConfigurationSet=function(e){this.emptyConfigForm=this.fb.group({})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-node-empty-config",template:"
"}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),T=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.attributesConfigForm},r.prototype.onConfigurationSet=function(e){this.attributesConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]],notifyDevice:[!e||e.scope,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),q=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.timeseriesConfigForm},r.prototype.onConfigurationSet=function(e){this.timeseriesConfigForm=this.fb.group({defaultTTL:[e?e.defaultTTL:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),S=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcRequestConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcRequestConfigForm=this.fb.group({timeoutInSeconds:[e?e.timeoutInSeconds:null,[i.Validators.required,i.Validators.min(0)]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),I=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.logConfigForm},r.prototype.onConfigurationSet=function(e){this.logConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.logConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"string",this.translate.instant("tb.rulenode.to-string"),"ToString",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/log_node_script_fn").subscribe((function(t){t&&e.logConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-log-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),k=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.assignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.assignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],createCustomerIfNotExists:[!!e&&e.createCustomerIfNotExists,[]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),N=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.clearAlarmConfigForm},r.prototype.onConfigurationSet=function(e){this.clearAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],alarmType:[e?e.alarmType:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.clearAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/clear_alarm_node_script_fn").subscribe((function(t){t&&e.clearAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-clear-alarm-config",template:'
\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'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),V=function(e){function r(t,r,n,o){var i=e.call(this,t)||this;return i.store=t,i.fb=r,i.nodeScriptTestService=n,i.translate=o,i.alarmSeverities=Object.keys(a.AlarmSeverity),i.alarmSeverityTranslationMap=a.alarmSeverityTranslations,i.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],i}return b(r,e),r.prototype.configForm=function(){return this.createAlarmConfigForm},r.prototype.onConfigurationSet=function(e){var t=this;this.createAlarmConfigForm=this.fb.group({alarmDetailsBuildJs:[e?e.alarmDetailsBuildJs:null,[i.Validators.required]],useMessageAlarmData:[!!e&&e.useMessageAlarmData,[]],alarmType:[e?e.alarmType:null,[]],severity:[e?e.severity:null,[]],propagate:[!!e&&e.propagate,[]],relationTypes:[e?e.relationTypes:null,[]],dynamicSeverity:!1}),this.createAlarmConfigForm.get("dynamicSeverity").valueChanges.subscribe((function(e){e?t.createAlarmConfigForm.get("severity").patchValue("",{emitEvent:!1}):t.createAlarmConfigForm.get("severity").patchValue(t.alarmSeverities[0],{emitEvent:!1})}))},r.prototype.validatorTriggers=function(){return["useMessageAlarmData"]},r.prototype.updateValidators=function(e){this.createAlarmConfigForm.get("useMessageAlarmData").value?(this.createAlarmConfigForm.get("alarmType").setValidators([]),this.createAlarmConfigForm.get("severity").setValidators([])):(this.createAlarmConfigForm.get("alarmType").setValidators([i.Validators.required]),this.createAlarmConfigForm.get("severity").setValidators([i.Validators.required])),this.createAlarmConfigForm.get("alarmType").updateValueAndValidity({emitEvent:e}),this.createAlarmConfigForm.get("severity").updateValueAndValidity({emitEvent:e})},r.prototype.testScript=function(){var e=this,t=this.createAlarmConfigForm.get("alarmDetailsBuildJs").value;this.nodeScriptTestService.testNodeScript(t,"json",this.translate.instant("tb.rulenode.details"),"Details",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/create_alarm_node_script_fn").subscribe((function(t){t&&e.createAlarmConfigForm.get("alarmDetailsBuildJs").setValue(t)}))},r.prototype.removeKey=function(e,t){var r=this.createAlarmConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.createAlarmConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.createAlarmConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.createAlarmConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-action-node-create-alarm-config",template:'
\n \n \n \n
\n \n
\n
\n \n {{ \'tb.rulenode.use-message-alarm-data\' | translate }}\n \n \n {{ \'tb.rulenode.use-dynamically-change-the-severity-of-alar\' | translate }}\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.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\' | translate }}\n \n \n {{ \'tb.rulenode.alarm-severity-required\' | translate }}\n \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
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),E=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.createRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.createRelationConfigForm=this.fb.group({direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[i.Validators.required]],entityNamePattern:[e?e.entityNamePattern:null,[]],entityTypePattern:[e?e.entityTypePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],createEntityIfNotExists:[!!e&&e.createEntityIfNotExists,[]],removeCurrentRelations:[!!e&&e.removeCurrentRelations,[]],changeOriginatorToRelatedEntity:[!!e&&e.changeOriginatorToRelatedEntity,[]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["entityType"]},r.prototype.updateValidators=function(e){var t=this.createRelationConfigForm.get("entityType").value;t?this.createRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.createRelationConfigForm.get("entityNamePattern").setValidators([]),!t||t!==a.EntityType.DEVICE&&t!==a.EntityType.ASSET?this.createRelationConfigForm.get("entityTypePattern").setValidators([]):this.createRelationConfigForm.get("entityTypePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]),this.createRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e}),this.createRelationConfigForm.get("entityTypePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e.entityTypePattern=e.entityTypePattern?e.entityTypePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),A=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgDelayConfigForm},r.prototype.onConfigurationSet=function(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,[i.Validators.required,i.Validators.min(1),i.Validators.max(1e5)]]})},r.prototype.validatorTriggers=function(){return["useMetadataPeriodInSecondsPatterns"]},r.prototype.updateValidators=function(e){this.msgDelayConfigForm.get("useMetadataPeriodInSecondsPatterns").value?(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([i.Validators.required]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([])):(this.msgDelayConfigForm.get("periodInSecondsPattern").setValidators([]),this.msgDelayConfigForm.get("periodInSeconds").setValidators([i.Validators.required,i.Validators.min(0)])),this.msgDelayConfigForm.get("periodInSecondsPattern").updateValueAndValidity({emitEvent:e}),this.msgDelayConfigForm.get("periodInSeconds").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),L=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n}return b(r,e),r.prototype.configForm=function(){return this.deleteRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.deleteRelationConfigForm=this.fb.group({deleteForSingleEntity:[!!e&&e.deleteForSingleEntity,[]],direction:[e?e.direction:null,[i.Validators.required]],entityType:[e?e.entityType:null,[]],entityNamePattern:[e?e.entityNamePattern:null,[]],relationType:[e?e.relationType:null,[i.Validators.required]],entityCacheExpiration:[e?e.entityCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.validatorTriggers=function(){return["deleteForSingleEntity","entityType"]},r.prototype.updateValidators=function(e){var t=this.deleteRelationConfigForm.get("deleteForSingleEntity").value,r=this.deleteRelationConfigForm.get("entityType").value;t?this.deleteRelationConfigForm.get("entityType").setValidators([i.Validators.required]):this.deleteRelationConfigForm.get("entityType").setValidators([]),t&&r?this.deleteRelationConfigForm.get("entityNamePattern").setValidators([i.Validators.required,i.Validators.pattern(/.*\S.*/)]):this.deleteRelationConfigForm.get("entityNamePattern").setValidators([]),this.deleteRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:!1}),this.deleteRelationConfigForm.get("entityNamePattern").updateValueAndValidity({emitEvent:e})},r.prototype.prepareOutputConfig=function(e){return e.entityNamePattern=e.entityNamePattern?e.entityNamePattern.trim():null,e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),P=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.generatorConfigForm},r.prototype.onConfigurationSet=function(e){this.generatorConfigForm=this.fb.group({msgCount:[e?e.msgCount:null,[i.Validators.required,i.Validators.min(0)]],periodInSeconds:[e?e.periodInSeconds:null,[i.Validators.required,i.Validators.min(1)]],originator:[e?e.originator:null,[]],jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.prepareInputConfig=function(e){return e&&(e.originatorId&&e.originatorType?e.originator={id:e.originatorId,entityType:e.originatorType}:e.originator=null,delete e.originatorId,delete e.originatorType),e},r.prototype.prepareOutputConfig=function(e){return e.originator?(e.originatorId=e.originator.id,e.originatorType=e.originator.entityType):(e.originatorId=null,e.originatorType=null),delete e.originator,e},r.prototype.testScript=function(){var e=this,t=this.generatorConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"generate",this.translate.instant("tb.rulenode.generator"),"Generate",["prevMsg","prevMetadata","prevMsgType"],this.ruleNodeId,"rulenode/generator_node_script_fn").subscribe((function(t){t&&e.generatorConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent);!function(e){e.CUSTOMER="CUSTOMER",e.TENANT="TENANT",e.RELATED="RELATED",e.ALARM_ORIGINATOR="ALARM_ORIGINATOR"}(F||(F={}));var M,w=new Map([[F.CUSTOMER,"tb.rulenode.originator-customer"],[F.TENANT,"tb.rulenode.originator-tenant"],[F.RELATED,"tb.rulenode.originator-related"],[F.ALARM_ORIGINATOR,"tb.rulenode.originator-alarm-originator"]]);!function(e){e.CIRCLE="CIRCLE",e.POLYGON="POLYGON"}(M||(M={}));var R,D=new Map([[M.CIRCLE,"tb.rulenode.perimeter-circle"],[M.POLYGON,"tb.rulenode.perimeter-polygon"]]);!function(e){e.MILLISECONDS="MILLISECONDS",e.SECONDS="SECONDS",e.MINUTES="MINUTES",e.HOURS="HOURS",e.DAYS="DAYS"}(R||(R={}));var O,K=new Map([[R.MILLISECONDS,"tb.rulenode.time-unit-milliseconds"],[R.SECONDS,"tb.rulenode.time-unit-seconds"],[R.MINUTES,"tb.rulenode.time-unit-minutes"],[R.HOURS,"tb.rulenode.time-unit-hours"],[R.DAYS,"tb.rulenode.time-unit-days"]]);!function(e){e.METER="METER",e.KILOMETER="KILOMETER",e.FOOT="FOOT",e.MILE="MILE",e.NAUTICAL_MILE="NAUTICAL_MILE"}(O||(O={}));var B,H=new Map([[O.METER,"tb.rulenode.range-unit-meter"],[O.KILOMETER,"tb.rulenode.range-unit-kilometer"],[O.FOOT,"tb.rulenode.range-unit-foot"],[O.MILE,"tb.rulenode.range-unit-mile"],[O.NAUTICAL_MILE,"tb.rulenode.range-unit-nautical-mile"]]);!function(e){e.TITLE="TITLE",e.COUNTRY="COUNTRY",e.STATE="STATE",e.ZIP="ZIP",e.ADDRESS="ADDRESS",e.ADDRESS2="ADDRESS2",e.PHONE="PHONE",e.EMAIL="EMAIL",e.ADDITIONAL_INFO="ADDITIONAL_INFO"}(B||(B={}));var G,j,U,_=new Map([[B.TITLE,"tb.rulenode.entity-details-title"],[B.COUNTRY,"tb.rulenode.entity-details-country"],[B.STATE,"tb.rulenode.entity-details-state"],[B.ZIP,"tb.rulenode.entity-details-zip"],[B.ADDRESS,"tb.rulenode.entity-details-address"],[B.ADDRESS2,"tb.rulenode.entity-details-address2"],[B.PHONE,"tb.rulenode.entity-details-phone"],[B.EMAIL,"tb.rulenode.entity-details-email"],[B.ADDITIONAL_INFO,"tb.rulenode.entity-details-additional_info"]]);!function(e){e.FIRST="FIRST",e.LAST="LAST",e.ALL="ALL"}(G||(G={})),function(e){e.ASC="ASC",e.DESC="DESC"}(j||(j={})),function(e){e.STANDARD="STANDARD",e.FIFO="FIFO"}(U||(U={}));var z,Q=new Map([[U.STANDARD,"tb.rulenode.sqs-queue-standard"],[U.FIFO,"tb.rulenode.sqs-queue-fifo"]]),$=["anonymous","basic","cert.PEM"],W=new Map([["anonymous","tb.rulenode.credentials-anonymous"],["basic","tb.rulenode.credentials-basic"],["cert.PEM","tb.rulenode.credentials-pem"]]),J=["sas","cert.PEM"],Y=new Map([["sas","tb.rulenode.credentials-sas"],["cert.PEM","tb.rulenode.credentials-pem"]]);!function(e){e.GET="GET",e.POST="POST",e.PUT="PUT",e.DELETE="DELETE"}(z||(z={}));var Z=["US-ASCII","ISO-8859-1","UTF-8","UTF-16BE","UTF-16LE","UTF-16"],X=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"]]),ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=H,n.timeUnits=Object.keys(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.geoActionConfigForm},r.prototype.onConfigurationSet=function(e){this.geoActionConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType: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,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minInsideDurationTimeUnit:[e?e.minInsideDurationTimeUnit:null,[i.Validators.required]],minOutsideDuration:[e?e.minOutsideDuration:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]],minOutsideDurationTimeUnit:[e?e.minOutsideDurationTimeUnit:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoActionConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoActionConfigForm.get("perimeterType").value;t?this.geoActionConfigForm.get("perimeterType").setValidators([]):this.geoActionConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.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([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoActionConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoActionConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoActionConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoActionConfigForm.get("polygonsDefinition").setValidators([]):this.geoActionConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoActionConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.msgCountConfigForm},r.prototype.onConfigurationSet=function(e){this.msgCountConfigForm=this.fb.group({interval:[e?e.interval:null,[i.Validators.required,i.Validators.min(1)]],telemetryPrefix:[e?e.telemetryPrefix:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.rpcReplyConfigForm},r.prototype.onConfigurationSet=function(e){this.rpcReplyConfigForm=this.fb.group({requestIdMetaDataAttribute:[e?e.requestIdMetaDataAttribute:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-rpc-reply-config",template:'
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.saveToCustomTableConfigForm},r.prototype.onConfigurationSet=function(e){this.saveToCustomTableConfigForm=this.fb.group({tableName:[e?e.tableName:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.prototype.prepareOutputConfig=function(e){return e.tableName=e.tableName.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ae=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.translate=r,o.injector=n,o.fb=a,o.propagateChange=null,o.valueChangeSubscription=null,o}var a;return b(r,e),a=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){this.ngControl=this.injector.get(i.NgControl),null!=this.ngControl&&(this.ngControl.valueAccessor=this),this.kvListFormGroup=this.fb.group({}),this.kvListFormGroup.addControl("keyVals",this.fb.array([]))},r.prototype.keyValsFormArray=function(){return this.kvListFormGroup.get("keyVals")},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.kvListFormGroup.disable({emitEvent:!1}):this.kvListFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t,r,n=this;this.valueChangeSubscription&&this.valueChangeSubscription.unsubscribe();var a=[];if(e)try{for(var o=v(Object.keys(e)),l=o.next();!l.done;l=o.next()){var s=l.value;Object.prototype.hasOwnProperty.call(e,s)&&a.push(this.fb.group({key:[s,[i.Validators.required]],value:[e[s],[i.Validators.required]]}))}}catch(e){t={error:e}}finally{try{l&&!l.done&&(r=o.return)&&r.call(o)}finally{if(t)throw t.error}}this.kvListFormGroup.setControl("keyVals",this.fb.array(a)),this.valueChangeSubscription=this.kvListFormGroup.valueChanges.subscribe((function(){n.updateModel()}))},r.prototype.removeKeyVal=function(e){this.kvListFormGroup.get("keyVals").removeAt(e)},r.prototype.addKeyVal=function(){this.kvListFormGroup.get("keyVals").push(this.fb.group({key:["",[i.Validators.required]],value:["",[i.Validators.required]]}))},r.prototype.validate=function(e){return!this.kvListFormGroup.get("keyVals").value.length&&this.required?{kvMapRequired:!0}:this.kvListFormGroup.valid?null:{kvFieldsRequired:!0}},r.prototype.updateModel=function(){var e=this.kvListFormGroup.get("keyVals").value;if(this.required&&!e.length||!this.kvListFormGroup.valid)this.propagateChange(null);else{var t={};e.forEach((function(e){t[e.key]=e.value})),this.propagateChange(t)}},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:t.Injector},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",String)],r.prototype,"requiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"keyRequiredText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valText",void 0),h([t.Input(),C("design:type",String)],r.prototype,"valRequiredText",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=a=h([t.Component({selector:"tb-kv-map-config",template:'
\n
\n {{ keyText | translate }}\n {{ valText | translate }}\n \n
\n
\n
\n \n \n \n \n {{ keyRequiredText | translate }}\n \n \n \n \n \n \n {{ valRequiredText | translate }}\n \n \n \n
\n
\n \n
\n \n
\n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return a})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return a})),multi:!0}],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:rgba(0,0,0,.54);font-size:12px;font-weight:700;white-space:nowrap}:host .tb-kv-map-config .body{padding-left:5px;padding-right:5px;padding-bottom:20px;max-height:300px;overflow:auto}:host .tb-kv-map-config .body .row{padding-top:5px;max-height:40px}:host .tb-kv-map-config .body .cell{padding-left:5px;padding-right:5px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell{margin:0;max-height:40px}:host ::ng-deep .tb-kv-map-config .body mat-form-field.cell .mat-form-field-infix{border-top:0}:host ::ng-deep .tb-kv-map-config .body button.mat-button{margin:0}"]}),C("design:paramtypes",[o.Store,n.TranslateService,t.Injector,i.FormBuilder])],r)}(a.PageComponent),oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.entityType=a.EntityType,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.deviceRelationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],relationType:[null],deviceTypes:[null,[i.Validators.required]]}),this.deviceRelationsQueryFormGroup.valueChanges.subscribe((function(t){e.deviceRelationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.deviceRelationsQueryFormGroup.disable({emitEvent:!1}):this.deviceRelationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.deviceRelationsQueryFormGroup.reset(e,{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-device-relations-query-config",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-type
\n \n \n
device.device-types
\n \n \n
\n',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),ie=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.directionTypes=Object.keys(a.EntitySearchDirection),n.directionTypeTranslations=a.entitySearchDirectionTranslations,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.relationsQueryFormGroup=this.fb.group({fetchLastLevelOnly:[!1,[]],direction:[null,[i.Validators.required]],maxLevel:[null,[]],filters:[null]}),this.relationsQueryFormGroup.valueChanges.subscribe((function(t){e.relationsQueryFormGroup.valid?e.propagateChange(t):e.propagateChange(null)}))},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.relationsQueryFormGroup.disable({emitEvent:!1}):this.relationsQueryFormGroup.enable({emitEvent:!1})},r.prototype.writeValue=function(e){this.relationsQueryFormGroup.reset(e||{},{emitEvent:!1})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),r=n=h([t.Component({selector:"tb-relations-query-config",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',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),le=function(e){function r(t,r,n,o){var i,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.truncate=n,s.fb=o,s.placeholder="tb.rulenode.message-type",s.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],s.messageTypes=[],s.messageTypesList=[],s.searchText="",s.propagateChange=function(e){},s.messageTypeConfigForm=s.fb.group({messageType:[null]});try{for(var u=v(Object.keys(a.MessageType)),d=u.next();!d.done;d=u.next()){var p=d.value;s.messageTypesList.push({name:a.messageTypeNames.get(a.MessageType[p]),value:p})}}catch(e){i={error:e}}finally{try{d&&!d.done&&(l=u.return)&&l.call(u)}finally{if(i)throw i.error}}return s}var l;return b(r,e),l=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.ngOnInit=function(){var e=this;this.filteredMessageTypes=this.messageTypeConfigForm.get("messageType").valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(t){return e.fetchMessageTypes(t)})),f.share())},r.prototype.ngAfterViewInit=function(){},r.prototype.setDisabledState=function(e){this.disabled=e,this.disabled?this.messageTypeConfigForm.disable({emitEvent:!1}):this.messageTypeConfigForm.enable({emitEvent:!1})},r.prototype.writeValue=function(e){var t=this;this.searchText="",this.messageTypes.length=0,e&&e.forEach((function(e){var r=t.messageTypesList.find((function(t){return t.value===e}));r?t.messageTypes.push({name:r.name,value:r.value}):t.messageTypes.push({name:e,value:e})}))},r.prototype.displayMessageTypeFn=function(e){return e?e.name:void 0},r.prototype.textIsNotEmpty=function(e){return!!(e&&null!=e&&e.length>0)},r.prototype.createMessageType=function(e,t){e.preventDefault(),this.transformMessageType(t)},r.prototype.add=function(e){this.transformMessageType(e.value)},r.prototype.fetchMessageTypes=function(e){if(this.searchText=e,this.searchText&&this.searchText.length){var t=this.searchText.toUpperCase();return c.of(this.messageTypesList.filter((function(e){return e.name.toUpperCase().includes(t)})))}return c.of(this.messageTypesList)},r.prototype.transformMessageType=function(e){if((e||"").trim()){var t=null,r=e.trim(),n=this.messageTypesList.find((function(e){return e.name===r}));(t=n?{name:n.name,value:n.value}:{name:r,value:r})&&this.addMessageType(t)}this.clear("")},r.prototype.remove=function(e){var t=this.messageTypes.indexOf(e);t>=0&&(this.messageTypes.splice(t,1),this.updateModel())},r.prototype.selected=function(e){this.addMessageType(e.option.value),this.clear("")},r.prototype.addMessageType=function(e){-1===this.messageTypes.findIndex((function(t){return t.value===e.value}))&&(this.messageTypes.push(e),this.updateModel())},r.prototype.onFocus=function(){this.messageTypeConfigForm.get("messageType").updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.messageTypeInput.nativeElement.value=e,this.messageTypeConfigForm.get("messageType").patchValue(null,{emitEvent:!0}),setTimeout((function(){t.messageTypeInput.nativeElement.blur(),t.messageTypeInput.nativeElement.focus()}),0)},r.prototype.updateModel=function(){var e=this.messageTypes.map((function(e){return e.value}));this.required?(this.chipList.errorState=!e.length,this.propagateChange(e.length>0?e:null)):(this.chipList.errorState=!1,this.propagateChange(e))},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:a.TruncatePipe},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",String)],r.prototype,"label",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"placeholder",void 0),h([t.Input(),C("design:type",Boolean)],r.prototype,"disabled",void 0),h([t.ViewChild("chipList",{static:!1}),C("design:type",d.MatChipList)],r.prototype,"chipList",void 0),h([t.ViewChild("messageTypeAutocomplete",{static:!1}),C("design:type",p.MatAutocomplete)],r.prototype,"matAutocomplete",void 0),h([t.ViewChild("messageTypeInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"messageTypeInput",void 0),r=l=h([t.Component({selector:"tb-message-types-config",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 {{ translate.get(\'tb.rulenode.no-message-type-matching\',\n {messageType: truncate.transform(searchText, true, 6, '...')}) | async }}\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',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return l})),multi:!0}]}),C("design:paramtypes",[o.Store,n.TranslateService,a.TruncatePipe,i.FormBuilder])],r)}(a.PageComponent),se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.subscriptions=[],n.disableCertPemCredentials=!1,n.passwordFieldRquired=!0,n.allCredentialsTypes=$,n.credentialsTypeTranslationsMap=W,n.propagateChange=null,n}var n;return b(r,e),n=r,Object.defineProperty(r.prototype,"required",{get:function(){return this.requiredValue},set:function(e){this.requiredValue=u.coerceBooleanProperty(e)},enumerable:!0,configurable:!0}),r.prototype.ngOnInit=function(){var e=this;this.credentialsConfigFormGroup=this.fb.group({type:[null,[i.Validators.required]],username:[null,[]],password:[null,[]],caCert:[null,[]],caCertFileName:[null,[]],privateKey:[null,[]],privateKeyFileName:[null,[]],cert:[null,[]],certFileName:[null,[]]}),this.subscriptions.push(this.credentialsConfigFormGroup.valueChanges.pipe(f.distinctUntilChanged()).subscribe((function(){e.updateView()}))),this.subscriptions.push(this.credentialsConfigFormGroup.get("type").valueChanges.subscribe((function(){e.credentialsTypeChanged()})))},r.prototype.ngOnChanges=function(e){var t,r,n=this;try{for(var a=v(Object.keys(e)),o=a.next();!o.done;o=a.next()){var i=o.value,l=e[i];if(!l.firstChange&&l.currentValue!==l.previousValue)if(l.currentValue&&"disableCertPemCredentials"===i)"cert.PEM"===this.credentialsConfigFormGroup.get("type").value&&setTimeout((function(){n.credentialsConfigFormGroup.get("type").patchValue("anonymous",{emitEvent:!0})}))}}catch(e){t={error:e}}finally{try{o&&!o.done&&(r=a.return)&&r.call(a)}finally{if(t)throw t.error}}},r.prototype.ngOnDestroy=function(){this.subscriptions.forEach((function(e){return e.unsubscribe()}))},r.prototype.writeValue=function(e){s.isDefinedAndNotNull(e)&&(this.credentialsConfigFormGroup.reset(e,{emitEvent:!1}),this.updateValidators(!1))},r.prototype.setDisabledState=function(e){e?this.credentialsConfigFormGroup.disable():(this.credentialsConfigFormGroup.enable(),this.updateValidators())},r.prototype.updateView=function(){var e=this.credentialsConfigFormGroup.value,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)},r.prototype.registerOnChange=function(e){this.propagateChange=e},r.prototype.registerOnTouched=function(e){},r.prototype.validate=function(e){return this.credentialsConfigFormGroup.valid?null:{credentialsConfig:{valid:!1}}},r.prototype.credentialsTypeChanged=function(){this.credentialsConfigFormGroup.patchValue({username:null,password:null,caCert:null,caCertFileName:null,privateKey:null,privateKeyFileName:null,cert:null,certFileName:null}),this.updateValidators()},r.prototype.updateValidators=function(e){void 0===e&&(e=!1);var 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([i.Validators.required]),this.credentialsConfigFormGroup.get("password").setValidators(this.passwordFieldRquired?[i.Validators.required]:[]);break;case"cert.PEM":this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(i.Validators.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})},r.prototype.requiredFilesSelected=function(e,t){return void 0===t&&(t=null),function(r){return t||(t=[Object.keys(r.controls)]),(null==r?void 0:r.controls)&&t.some((function(t){return t.every((function(t){return!e(r.controls[t])}))}))?null:{notAllRequiredFilesSelected:!0}}},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},h([t.Input(),C("design:type",Boolean),C("design:paramtypes",[Boolean])],r.prototype,"required",null),h([t.Input(),C("design:type",Object)],r.prototype,"disableCertPemCredentials",void 0),h([t.Input(),C("design:type",Object)],r.prototype,"passwordFieldRquired",void 0),r=n=h([t.Component({selector:"tb-credentials-config",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',providers:[{provide:i.NG_VALUE_ACCESSOR,useExisting:t.forwardRef((function(){return n})),multi:!0},{provide:i.NG_VALIDATORS,useExisting:t.forwardRef((function(){return n})),multi:!0}]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.PageComponent),me=function(){function e(e){this.sanitizer=e}return e.prototype.transform=function(e){return this.sanitizer.bypassSecurityTrustHtml(e)},e.ctorParameters=function(){return[{type:g.DomSanitizer}]},e=h([t.Pipe({name:"safeHtml"}),C("design:paramtypes",[g.DomSanitizer])],e)}(),ue=function(){function e(){}return e=h([t.NgModule({declarations:[ae,oe,ie,le,se,me],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule],exports:[ae,oe,ie,le,se,me]})],e)}(),de=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.unassignCustomerConfigForm},r.prototype.onConfigurationSet=function(e){this.unassignCustomerConfigForm=this.fb.group({customerNamePattern:[e?e.customerNamePattern:null,[i.Validators.required,i.Validators.pattern(/.*\S.*/)]],customerCacheExpiration:[e?e.customerCacheExpiration:null,[i.Validators.required,i.Validators.min(0)]]})},r.prototype.prepareOutputConfig=function(e){return e.customerNamePattern=e.customerNamePattern.trim(),e},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),pe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.snsConfigForm},r.prototype.onConfigurationSet=function(e){this.snsConfigForm=this.fb.group({topicArnPattern:[e?e.topicArnPattern:null,[i.Validators.required]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.sqsQueueType=U,n.sqsQueueTypes=Object.keys(U),n.sqsQueueTypeTranslationsMap=Q,n}return b(r,e),r.prototype.configForm=function(){return this.sqsConfigForm},r.prototype.onConfigurationSet=function(e){this.sqsConfigForm=this.fb.group({queueType:[e?e.queueType:null,[i.Validators.required]],queueUrlPattern:[e?e.queueUrlPattern:null,[i.Validators.required]],delaySeconds:[e?e.delaySeconds:null,[i.Validators.min(0),i.Validators.max(900)]],messageAttributes:[e?e.messageAttributes:null,[]],accessKeyId:[e?e.accessKeyId:null,[i.Validators.required]],secretAccessKey:[e?e.secretAccessKey:null,[i.Validators.required]],region:[e?e.region:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.pubSubConfigForm},r.prototype.onConfigurationSet=function(e){this.pubSubConfigForm=this.fb.group({projectId:[e?e.projectId:null,[i.Validators.required]],topicName:[e?e.topicName:null,[i.Validators.required]],serviceAccountKey:[e?e.serviceAccountKey:null,[i.Validators.required]],serviceAccountKeyFileName:[e?e.serviceAccountKeyFileName:null,[i.Validators.required]],messageAttributes:[e?e.messageAttributes:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.ackValues=["all","-1","0","1"],n.ToByteStandartCharsetTypesValues=Z,n.ToByteStandartCharsetTypeTranslationMap=X,n}return b(r,e),r.prototype.configForm=function(){return this.kafkaConfigForm},r.prototype.onConfigurationSet=function(e){this.kafkaConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],bootstrapServers:[e?e.bootstrapServers:null,[i.Validators.required]],retries:[e?e.retries:null,[i.Validators.min(0)]],batchSize:[e?e.batchSize:null,[i.Validators.min(0)]],linger:[e?e.linger:null,[i.Validators.min(0)]],bufferMemory:[e?e.bufferMemory:null,[i.Validators.min(0)]],acks:[e?e.acks:null,[i.Validators.required]],keySerializer:[e?e.keySerializer:null,[i.Validators.required]],valueSerializer:[e?e.valueSerializer:null,[i.Validators.required]],otherProperties:[e?e.otherProperties:null,[]],addMetadataKeyValuesAsKafkaHeaders:[!!e&&e.addMetadataKeyValuesAsKafkaHeaders,[]],kafkaHeadersCharset:[e?e.kafkaHeadersCharset:null,[]]})},r.prototype.validatorTriggers=function(){return["addMetadataKeyValuesAsKafkaHeaders"]},r.prototype.updateValidators=function(e){this.kafkaConfigForm.get("addMetadataKeyValuesAsKafkaHeaders").value?this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([i.Validators.required]):this.kafkaConfigForm.get("kafkaHeadersCharset").setValidators([]),this.kafkaConfigForm.get("kafkaHeadersCharset").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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.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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ye=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.mqttConfigForm},r.prototype.onConfigurationSet=function(e){this.mqttConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:[e?e.credentials:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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 \n \n {{ \'tb.rulenode.clean-session\' | translate }}\n \n \n {{ \'tb.rulenode.enable-ssl\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.messageProperties=[null,"BASIC","TEXT_PLAIN","MINIMAL_BASIC","MINIMAL_PERSISTENT_BASIC","PERSISTENT_BASIC","PERSISTENT_TEXT_PLAIN"],n}return b(r,e),r.prototype.configForm=function(){return this.rabbitMqConfigForm},r.prototype.onConfigurationSet=function(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,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.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,[i.Validators.min(0)]],handshakeTimeout:[e?e.handshakeTimeout:null,[i.Validators.min(0)]],clientProperties:[e?e.clientProperties:null,[]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),he=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.proxySchemes=["http","https"],n.httpRequestTypes=Object.keys(z),n}return b(r,e),r.prototype.configForm=function(){return this.restApiCallConfigForm},r.prototype.onConfigurationSet=function(e){this.restApiCallConfigForm=this.fb.group({restEndpointUrlPattern:[e?e.restEndpointUrlPattern:null,[i.Validators.required]],requestMethod:[e?e.requestMethod:null,[i.Validators.required]],useSimpleClientHttpFactory:[!!e&&e.useSimpleClientHttpFactory,[]],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,[i.Validators.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,[]]})},r.prototype.validatorTriggers=function(){return["useSimpleClientHttpFactory","useRedisQueueForMsgPersistence","enableProxy","useSystemProxyProperties"]},r.prototype.updateValidators=function(e){var t=this.restApiCallConfigForm.get("useSimpleClientHttpFactory").value,r=this.restApiCallConfigForm.get("useRedisQueueForMsgPersistence").value,n=this.restApiCallConfigForm.get("enableProxy").value,a=this.restApiCallConfigForm.get("useSystemProxyProperties").value;n&&!a?(this.restApiCallConfigForm.get("proxyHost").setValidators(n?[i.Validators.required]:[]),this.restApiCallConfigForm.get("proxyPort").setValidators(n?[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]:[])):(this.restApiCallConfigForm.get("proxyHost").setValidators([]),this.restApiCallConfigForm.get("proxyPort").setValidators([]),t?this.restApiCallConfigForm.get("readTimeoutMs").setValidators([]):this.restApiCallConfigForm.get("readTimeoutMs").setValidators([i.Validators.min(0)])),r?this.restApiCallConfigForm.get("maxQueueSize").setValidators([i.Validators.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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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 \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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ce=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.smtpProtocols=["smtp","smtps"],n.tlsVersions=["TLSv1","TLSv1.1","TLSv1.2","TLSv1.3"],n}return b(r,e),r.prototype.configForm=function(){return this.sendEmailConfigForm},r.prototype.onConfigurationSet=function(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,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmtpSettings","enableProxy"]},r.prototype.updateValidators=function(e){var t=this.sendEmailConfigForm.get("useSystemSmtpSettings").value,r=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([i.Validators.required]),this.sendEmailConfigForm.get("smtpHost").setValidators([i.Validators.required]),this.sendEmailConfigForm.get("smtpPort").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]),this.sendEmailConfigForm.get("timeout").setValidators([i.Validators.required,i.Validators.min(0)]),this.sendEmailConfigForm.get("proxyHost").setValidators(r?[i.Validators.required]:[]),this.sendEmailConfigForm.get("proxyPort").setValidators(r?[i.Validators.required,i.Validators.min(1),i.Validators.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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.serviceType=a.ServiceType.TB_RULE_ENGINE,n}return b(r,e),r.prototype.configForm=function(){return this.checkPointConfigForm},r.prototype.onConfigurationSet=function(e){this.checkPointConfigForm=this.fb.group({queueName:[e?e.queueName:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-node-check-point-config",template:'
\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Fe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allAzureIotHubCredentialsTypes=J,n.azureIotHubCredentialsTypeTranslationsMap=Y,n}return b(r,e),r.prototype.configForm=function(){return this.azureIotHubConfigForm},r.prototype.onConfigurationSet=function(e){this.azureIotHubConfigForm=this.fb.group({topicPattern:[e?e.topicPattern:null,[i.Validators.required]],host:[e?e.host:null,[i.Validators.required]],port:[e?e.port:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(65535)]],connectTimeoutSec:[e?e.connectTimeoutSec:null,[i.Validators.required,i.Validators.min(1),i.Validators.max(200)]],clientId:[e?e.clientId:null,[i.Validators.required]],cleanSession:[!!e&&e.cleanSession,[]],ssl:[!!e&&e.ssl,[]],credentials:this.fb.group({type:[e&&e.credentials?e.credentials.type:null,[i.Validators.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,[]]})})},r.prototype.prepareOutputConfig=function(e){var t=e.credentials.type;return"sas"===t&&(e.credentials={type:t,sasKey:e.credentials.sasKey,caCert:e.credentials.caCert,caCertFileName:e.credentials.caCertFileName}),e},r.prototype.validatorTriggers=function(){return["credentials.type"]},r.prototype.updateValidators=function(e){var t=this.azureIotHubConfigForm.get("credentials"),r=t.get("type").value;switch(e&&t.reset({type:r},{emitEvent:!1}),t.get("sasKey").setValidators([]),t.get("privateKey").setValidators([]),t.get("privateKeyFileName").setValidators([]),t.get("cert").setValidators([]),t.get("certFileName").setValidators([]),r){case"sas":t.get("sasKey").setValidators([i.Validators.required]);break;case"cert.PEM":t.get("privateKey").setValidators([i.Validators.required]),t.get("privateKeyFileName").setValidators([i.Validators.required]),t.get("cert").setValidators([i.Validators.required]),t.get("certFileName").setValidators([i.Validators.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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),xe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.deviceProfile},r.prototype.onConfigurationSet=function(e){this.deviceProfile=this.fb.group({persistAlarmRulesState:[!!e&&e.persistAlarmRulesState,i.Validators.required],fetchAlarmRulesStateOnStart:[!!e&&e.fetchAlarmRulesStateOnStart,i.Validators.required]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Te=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.sendSmsConfigForm},r.prototype.onConfigurationSet=function(e){this.sendSmsConfigForm=this.fb.group({numbersToTemplate:[e?e.numbersToTemplate:null,[i.Validators.required]],smsMessageTemplate:[e?e.smsMessageTemplate:null,[i.Validators.required]],useSystemSmsSettings:[!!e&&e.useSystemSmsSettings,[]],smsProviderConfiguration:[e?e.smsProviderConfiguration:null,[]]})},r.prototype.validatorTriggers=function(){return["useSystemSmsSettings"]},r.prototype.updateValidators=function(e){this.sendSmsConfigForm.get("useSystemSmsSettings").value?this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([]):this.sendSmsConfigForm.get("smsProviderConfiguration").setValidators([i.Validators.required]),this.sendSmsConfigForm.get("smsProviderConfiguration").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-action-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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),qe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.pushToEdgeConfigForm},r.prototype.onConfigurationSet=function(e){this.pushToEdgeConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Se=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.attributeScopes=Object.keys(a.AttributeScope),n.telemetryTypeTranslationsMap=a.telemetryTypeTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.pushToCloudConfigForm},r.prototype.onConfigurationSet=function(e){this.pushToCloudConfigForm=this.fb.group({scope:[e?e.scope:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ie=function(){function e(){}return e=h([t.NgModule({declarations:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te,qe,Se],imports:[r.CommonModule,a.SharedModule,l.HomeComponentsModule,ue],exports:[T,q,S,I,k,N,V,E,A,L,P,ee,te,re,ne,de,pe,ce,fe,ge,ye,be,he,Ce,ve,Fe,xe,Te,qe,Se]})],e)}(),ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.checkMessageConfigForm},r.prototype.onConfigurationSet=function(e){this.checkMessageConfigForm=this.fb.group({messageNames:[e?e.messageNames:null,[]],metadataNames:[e?e.metadataNames:null,[]],checkAllKeys:[!!e&&e.checkAllKeys,[]]})},r.prototype.validateConfig=function(){var e=this.checkMessageConfigForm.get("messageNames").value,t=this.checkMessageConfigForm.get("metadataNames").value;return e.length>0||t.length>0},r.prototype.removeMessageName=function(e){var t=this.checkMessageConfigForm.get("messageNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("messageNames").setValue(t,{emitEvent:!0}))},r.prototype.removeMetadataName=function(e){var t=this.checkMessageConfigForm.get("metadataNames").value,r=t.indexOf(e);r>=0&&(t.splice(r,1),this.checkMessageConfigForm.get("metadataNames").setValue(t,{emitEvent:!0}))},r.prototype.addMessageName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("messageNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("messageNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.prototype.addMetadataName=function(e){var t=e.input,r=e.value;if((r||"").trim()){r=r.trim();var n=this.checkMessageConfigForm.get("metadataNames").value;n&&-1!==n.indexOf(r)||(n||(n=[]),n.push(r),this.checkMessageConfigForm.get("metadataNames").setValue(n,{emitEvent:!0}))}t&&(t.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-check-message-config",template:'
\n \n \n \n \n \n {{messageName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\n \n \n \n \n \n {{metadataName}}\n close\n \n \n \n \n
tb.rulenode.separator-hint
\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}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ne=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.entitySearchDirection=Object.keys(a.EntitySearchDirection),n.entitySearchDirectionTranslationsMap=a.entitySearchDirectionTranslations,n}return b(r,e),r.prototype.configForm=function(){return this.checkRelationConfigForm},r.prototype.onConfigurationSet=function(e){this.checkRelationConfigForm=this.fb.group({checkForSingleEntity:[!!e&&e.checkForSingleEntity,[]],direction:[e?e.direction:null,[]],entityType:[e?e.entityType:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],entityId:[e?e.entityId:null,e&&e.checkForSingleEntity?[i.Validators.required]:[]],relationType:[e?e.relationType:null,[i.Validators.required]]})},r.prototype.validatorTriggers=function(){return["checkForSingleEntity"]},r.prototype.updateValidators=function(e){var t=this.checkRelationConfigForm.get("checkForSingleEntity").value;this.checkRelationConfigForm.get("entityType").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityType").updateValueAndValidity({emitEvent:e}),this.checkRelationConfigForm.get("entityId").setValidators(t?[i.Validators.required]:[]),this.checkRelationConfigForm.get("entityId").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ve=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.perimeterType=M,n.perimeterTypes=Object.keys(M),n.perimeterTypeTranslationMap=D,n.rangeUnits=Object.keys(O),n.rangeUnitTranslationMap=H,n}return b(r,e),r.prototype.configForm=function(){return this.geoFilterConfigForm},r.prototype.onConfigurationSet=function(e){this.geoFilterConfigForm=this.fb.group({latitudeKeyName:[e?e.latitudeKeyName:null,[i.Validators.required]],longitudeKeyName:[e?e.longitudeKeyName:null,[i.Validators.required]],fetchPerimeterInfoFromMessageMetadata:[!!e&&e.fetchPerimeterInfoFromMessageMetadata,[]],perimeterType:[e?e.perimeterType: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,[]]})},r.prototype.validatorTriggers=function(){return["fetchPerimeterInfoFromMessageMetadata","perimeterType"]},r.prototype.updateValidators=function(e){var t=this.geoFilterConfigForm.get("fetchPerimeterInfoFromMessageMetadata").value,r=this.geoFilterConfigForm.get("perimeterType").value;t?this.geoFilterConfigForm.get("perimeterType").setValidators([]):this.geoFilterConfigForm.get("perimeterType").setValidators([i.Validators.required]),t||r!==M.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([i.Validators.required,i.Validators.min(-90),i.Validators.max(90)]),this.geoFilterConfigForm.get("centerLongitude").setValidators([i.Validators.required,i.Validators.min(-180),i.Validators.max(180)]),this.geoFilterConfigForm.get("range").setValidators([i.Validators.required,i.Validators.min(0)]),this.geoFilterConfigForm.get("rangeUnit").setValidators([i.Validators.required])),t||r!==M.POLYGON?this.geoFilterConfigForm.get("polygonsDefinition").setValidators([]):this.geoFilterConfigForm.get("polygonsDefinition").setValidators([i.Validators.required]),this.geoFilterConfigForm.get("perimeterType").updateValueAndValidity({emitEvent:!1}),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})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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.fetch-perimeter-info-from-message-metadata\' | translate }}\n \n
\n \n tb.rulenode.perimeter-type\n \n \n {{ perimeterTypeTranslationMap.get(type) | translate }}\n \n \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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ee=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.messageTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.messageTypeConfigForm=this.fb.group({messageTypes:[e?e.messageTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-message-type-config",template:'
\n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ae=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.allowedEntityTypes=[a.EntityType.DEVICE,a.EntityType.ASSET,a.EntityType.ENTITY_VIEW,a.EntityType.TENANT,a.EntityType.CUSTOMER,a.EntityType.USER,a.EntityType.DASHBOARD,a.EntityType.RULE_CHAIN,a.EntityType.RULE_NODE],n}return b(r,e),r.prototype.configForm=function(){return this.originatorTypeConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorTypeConfigForm=this.fb.group({originatorTypes:[e?e.originatorTypes:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-filter-node-originator-type-config",template:'
\n \n \n \n
\n',styles:[":host ::ng-deep tb-entity-type-list .mat-form-field-flex{padding-top:0}:host ::ng-deep tb-entity-type-list .mat-form-field-infix{border-top:0}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Le=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"filter",this.translate.instant("tb.rulenode.filter"),"Filter",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/filter_node_script_fn").subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Pe=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.switchConfigForm},r.prototype.onConfigurationSet=function(e){this.switchConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.switchConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"switch",this.translate.instant("tb.rulenode.switch"),"Switch",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/switch_node_script_fn").subscribe((function(t){t&&e.switchConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-filter-node-switch-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),Me=function(e){function r(t,r,n){var o,l,s=e.call(this,t)||this;s.store=t,s.translate=r,s.fb=n,s.alarmStatusTranslationsMap=a.alarmStatusTranslations,s.alarmStatusList=[],s.searchText="",s.displayStatusFn=s.displayStatus.bind(s);try{for(var m=v(Object.keys(a.AlarmStatus)),u=m.next();!u.done;u=m.next()){var d=u.value;s.alarmStatusList.push(a.AlarmStatus[d])}}catch(e){o={error:e}}finally{try{u&&!u.done&&(l=m.return)&&l.call(m)}finally{if(o)throw o.error}}return s.statusFormControl=new i.FormControl(""),s.filteredAlarmStatus=s.statusFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return s.fetchAlarmStatus(e)})),f.share()),s}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.alarmStatusConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.statusFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.alarmStatusConfigForm=this.fb.group({alarmStatusList:[e?e.alarmStatusList:null,[i.Validators.required]]})},r.prototype.displayStatus=function(e){return e?this.translate.instant(a.alarmStatusTranslations.get(e)):void 0},r.prototype.fetchAlarmStatus=function(e){var t=this,r=this.getAlarmStatusList();if(this.searchText=e,this.searchText&&this.searchText.length){var n=this.searchText.toUpperCase();return c.of(r.filter((function(e){return t.translate.instant(a.alarmStatusTranslations.get(a.AlarmStatus[e])).toUpperCase().includes(n)})))}return c.of(r)},r.prototype.alarmStatusSelected=function(e){this.addAlarmStatus(e.option.value),this.clear("")},r.prototype.removeAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))}},r.prototype.addAlarmStatus=function(e){var t=this.alarmStatusConfigForm.get("alarmStatusList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.alarmStatusConfigForm.get("alarmStatusList").setValue(t))},r.prototype.getAlarmStatusList=function(){var e=this;return this.alarmStatusList.filter((function(t){return-1===e.alarmStatusConfigForm.get("alarmStatusList").value.indexOf(t)}))},r.prototype.onAlarmStatusInputFocus=function(){this.statusFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.alarmStatusInput.nativeElement.value=e,this.statusFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.alarmStatusInput.nativeElement.blur(),t.alarmStatusInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("alarmStatusInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"alarmStatusInput",void 0),r=h([t.Component({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'}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),we=function(){function e(){}return e=h([t.NgModule({declarations:[ke,Ne,Ve,Ee,Ae,Le,Pe,Me],imports:[r.CommonModule,a.SharedModule,ue],exports:[ke,Ne,Ve,Ee,Ae,Le,Pe,Me]})],e)}(),Re=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.customerAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.customerAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-customer-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),De=function(e){function r(t,r,n){var a,o,l=e.call(this,t)||this;l.store=t,l.translate=r,l.fb=n,l.entityDetailsTranslationsMap=_,l.entityDetailsList=[],l.searchText="",l.displayDetailsFn=l.displayDetails.bind(l);try{for(var s=v(Object.keys(B)),m=s.next();!m.done;m=s.next()){var u=m.value;l.entityDetailsList.push(B[u])}}catch(e){a={error:e}}finally{try{m&&!m.done&&(o=s.return)&&o.call(s)}finally{if(a)throw a.error}}return l.detailsFormControl=new i.FormControl(""),l.filteredEntityDetails=l.detailsFormControl.valueChanges.pipe(f.startWith(""),f.map((function(e){return e||""})),f.mergeMap((function(e){return l.fetchEntityDetails(e)})),f.share()),l}return b(r,e),r.prototype.ngOnInit=function(){e.prototype.ngOnInit.call(this)},r.prototype.configForm=function(){return this.entityDetailsConfigForm},r.prototype.prepareInputConfig=function(e){return this.searchText="",this.detailsFormControl.patchValue("",{emitEvent:!0}),e},r.prototype.onConfigurationSet=function(e){this.entityDetailsConfigForm=this.fb.group({detailsList:[e?e.detailsList:null,[i.Validators.required]],addToMetadata:[!!e&&e.addToMetadata,[]]})},r.prototype.displayDetails=function(e){return e?this.translate.instant(_.get(e)):void 0},r.prototype.fetchEntityDetails=function(e){var t=this;if(this.searchText=e,this.searchText&&this.searchText.length){var r=this.searchText.toUpperCase();return c.of(this.entityDetailsList.filter((function(e){return t.translate.instant(_.get(B[e])).toUpperCase().includes(r)})))}return c.of(this.entityDetailsList)},r.prototype.detailsFieldSelected=function(e){this.addDetailsField(e.option.value),this.clear("")},r.prototype.removeDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;if(t){var r=t.indexOf(e);r>=0&&(t.splice(r,1),this.entityDetailsConfigForm.get("detailsList").setValue(t))}},r.prototype.addDetailsField=function(e){var t=this.entityDetailsConfigForm.get("detailsList").value;t||(t=[]),-1===t.indexOf(e)&&(t.push(e),this.entityDetailsConfigForm.get("detailsList").setValue(t))},r.prototype.onEntityDetailsInputFocus=function(){this.detailsFormControl.updateValueAndValidity({onlySelf:!0,emitEvent:!0})},r.prototype.clear=function(e){var t=this;void 0===e&&(e=""),this.detailsInput.nativeElement.value=e,this.detailsFormControl.patchValue(null,{emitEvent:!0}),setTimeout((function(){t.detailsInput.nativeElement.blur(),t.detailsInput.nativeElement.focus()}),0)},r.ctorParameters=function(){return[{type:o.Store},{type:n.TranslateService},{type:i.FormBuilder}]},h([t.ViewChild("detailsInput",{static:!1}),C("design:type",t.ElementRef)],r.prototype,"detailsInput",void 0),r=h([t.Component({selector:"tb-enrichment-node-entity-details-config",template:'
\n \n tb.rulenode.entity-details\n \n \n \n {{entityDetailsTranslationsMap.get(details) | translate}}\n \n close\n \n \n \n \n \n \n \n \n
\n
\n tb.rulenode.no-entity-details-matching\n
\n
\n
\n
\n
\n \n \n {{ \'tb.rulenode.add-to-metadata\' | translate }}\n \n
tb.rulenode.add-to-metadata-hint
\n
\n',styles:[":host ::ng-deep mat-form-field.entity-fields-list .mat-form-field-wrapper{margin-bottom:-1.25em}"]}),C("design:paramtypes",[o.Store,n.TranslateService,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Oe=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.deviceAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.deviceAttributesConfigForm=this.fb.group({deviceRelationsQuery:[e?e.deviceRelationsQuery:null,[i.Validators.required]],tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.deviceAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.deviceAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.deviceAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.deviceAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-device-attributes-config",template:'
\n \n \n \n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ke=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.originatorAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorAttributesConfigForm=this.fb.group({tellFailureIfAbsent:[!!e&&e.tellFailureIfAbsent,[]],clientAttributeNames:[e?e.clientAttributeNames:null,[]],sharedAttributeNames:[e?e.sharedAttributeNames:null,[]],serverAttributeNames:[e?e.serverAttributeNames:null,[]],latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],getLatestValueWithTs:[!!e&&e.getLatestValueWithTs,[]]})},r.prototype.removeKey=function(e,t){var r=this.originatorAttributesConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.originatorAttributesConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.originatorAttributesConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.originatorAttributesConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-attributes-config",template:'
\n \n {{ \'tb.rulenode.tell-failure-if-absent\' | translate }}\n \n
tb.rulenode.tell-failure-if-absent-hint
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n {{ \'tb.rulenode.get-latest-value-with-ts\' | translate }}\n \n
\n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Be=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.originatorFieldsConfigForm},r.prototype.onConfigurationSet=function(e){this.originatorFieldsConfigForm=this.fb.group({fieldsMapping:[e?e.fieldsMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-originator-fields-config",template:'
\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),He=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n.aggregationTypes=a.AggregationType,n.aggregations=Object.keys(a.AggregationType),n.aggregationTypesTranslations=a.aggregationTranslations,n.fetchMode=G,n.fetchModes=Object.keys(G),n.samplingOrders=Object.keys(j),n.timeUnits=Object.values(R),n.timeUnitsTranslationMap=K,n}return b(r,e),r.prototype.configForm=function(){return this.getTelemetryFromDatabaseConfigForm},r.prototype.onConfigurationSet=function(e){this.getTelemetryFromDatabaseConfigForm=this.fb.group({latestTsKeyNames:[e?e.latestTsKeyNames:null,[]],aggregation:[e?e.aggregation:null,[i.Validators.required]],fetchMode:[e?e.fetchMode:null,[i.Validators.required]],orderBy:[e?e.orderBy:null,[]],limit:[e?e.limit:null,[]],useMetadataIntervalPatterns:[!!e&&e.useMetadataIntervalPatterns,[]],startInterval:[e?e.startInterval:null,[]],startIntervalTimeUnit:[e?e.startIntervalTimeUnit:null,[]],endInterval:[e?e.endInterval:null,[]],endIntervalTimeUnit:[e?e.endIntervalTimeUnit:null,[]],startIntervalPattern:[e?e.startIntervalPattern:null,[]],endIntervalPattern:[e?e.endIntervalPattern:null,[]]})},r.prototype.validatorTriggers=function(){return["fetchMode","useMetadataIntervalPatterns"]},r.prototype.updateValidators=function(e){var t=this.getTelemetryFromDatabaseConfigForm.get("fetchMode").value,r=this.getTelemetryFromDatabaseConfigForm.get("useMetadataIntervalPatterns").value;t&&t===G.ALL?(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([i.Validators.required,i.Validators.min(2),i.Validators.max(1e3)])):(this.getTelemetryFromDatabaseConfigForm.get("aggregation").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("orderBy").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("limit").setValidators([])),r?(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").setValidators([i.Validators.required])):(this.getTelemetryFromDatabaseConfigForm.get("startInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").setValidators([i.Validators.required]),this.getTelemetryFromDatabaseConfigForm.get("endInterval").setValidators([i.Validators.required,i.Validators.min(1),i.Validators.max(2147483647)]),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").setValidators([i.Validators.required]),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("startInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endInterval").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalTimeUnit").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("startIntervalPattern").updateValueAndValidity({emitEvent:e}),this.getTelemetryFromDatabaseConfigForm.get("endIntervalPattern").updateValueAndValidity({emitEvent:e})},r.prototype.removeKey=function(e,t){var r=this.getTelemetryFromDatabaseConfigForm.get(t).value,n=r.indexOf(e);n>=0&&(r.splice(n,1),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(r,{emitEvent:!0}))},r.prototype.addKey=function(e,t){var r=e.input,n=e.value;if((n||"").trim()){n=n.trim();var a=this.getTelemetryFromDatabaseConfigForm.get(t).value;a&&-1!==a.indexOf(n)||(a||(a=[]),a.push(n),this.getTelemetryFromDatabaseConfigForm.get(t).setValue(a,{emitEvent:!0}))}r&&(r.value="")},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-get-telemetry-from-database",template:'
\n \n \n \n \n \n {{key}}\n close\n \n \n \n \n \n \n tb.rulenode.fetch-mode\n \n \n {{ mode }}\n \n \n tb.rulenode.fetch-mode-hint\n \n
\n \n aggregation.function\n \n \n {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }}\n \n \n \n \n tb.rulenode.order-by\n \n \n {{ order }}\n \n \n tb.rulenode.order-by-hint\n \n \n tb.rulenode.limit\n \n tb.rulenode.limit-hint\n \n
\n \n {{ \'tb.rulenode.use-metadata-interval-patterns\' | translate }}\n \n
tb.rulenode.use-metadata-interval-patterns-hint
\n
\n
\n \n tb.rulenode.start-interval\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.start-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n tb.rulenode.end-interval\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.end-interval-time-unit\n \n \n {{ timeUnitsTranslationMap.get(timeUnit) | translate }}\n \n \n \n
\n
\n \n \n tb.rulenode.start-interval-pattern\n \n \n {{ \'tb.rulenode.start-interval-pattern-required\' | translate }}\n \n \n \n \n tb.rulenode.end-interval-pattern\n \n \n {{ \'tb.rulenode.end-interval-pattern-required\' | translate }}\n \n \n \n \n
\n',styles:[":host label.tb-title{margin-bottom:-10px}"]}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ge=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.relatedAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.relatedAttributesConfigForm=this.fb.group({relationsQuery:[e?e.relationsQuery:null,[i.Validators.required]],telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-related-attributes-config",template:'
\n \n \n \n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),je=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n}return b(r,e),r.prototype.configForm=function(){return this.tenantAttributesConfigForm},r.prototype.onConfigurationSet=function(e){this.tenantAttributesConfigForm=this.fb.group({telemetry:[!!e&&e.telemetry,[]],attrMapping:[e?e.attrMapping:null,[i.Validators.required]]})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-tenant-attributes-config",template:'
\n \n \n {{ \'tb.rulenode.latest-telemetry\' | translate }}\n \n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Ue=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.separatorKeysCodes=[m.ENTER,m.COMMA,m.SEMICOLON],n}return b(r,e),r.prototype.configForm=function(){return this.calculateDeltaConfigForm},r.prototype.onConfigurationSet=function(e){this.calculateDeltaConfigForm=this.fb.group({inputValueKey:[e?e.inputValueKey:null,[i.Validators.required]],outputValueKey:[e?e.outputValueKey:null,[i.Validators.required]],useCache:[e?e.useCache:null,[]],addPeriodBetweenMsgs:[!!e&&e.addPeriodBetweenMsgs,[]],periodValueKey:[e?e.periodValueKey:null,[]],round:[e?e.round:null,[i.Validators.min(0),i.Validators.max(15)]],tellFailureIfDeltaIsNegative:[e?e.tellFailureIfDeltaIsNegative:null,[]]})},r.prototype.updateValidators=function(e){this.calculateDeltaConfigForm.get("addPeriodBetweenMsgs").value?this.calculateDeltaConfigForm.get("periodValueKey").setValidators([i.Validators.required]):this.calculateDeltaConfigForm.get("periodValueKey").setValidators([]),this.calculateDeltaConfigForm.get("periodValueKey").updateValueAndValidity({emitEvent:e})},r.prototype.validatorTriggers=function(){return["addPeriodBetweenMsgs"]},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({selector:"tb-enrichment-node-calculate-delta-config",template:'
\n
\n \n tb.rulenode.input-value-key\n \n \n {{ \'tb.rulenode.input-value-key-required\' | translate }}\n \n \n \n tb.rulenode.output-value-key\n \n \n {{ \'tb.rulenode.output-value-key-required\' | translate }}\n \n \n \n tb.rulenode.round\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n {{ \'tb.rulenode.round-range\' | translate }}\n \n \n
\n \n {{ \'tb.rulenode.use-cache\' | translate }}\n \n \n {{ \'tb.rulenode.tell-failure-if-delta-is-negative\' | translate }}\n \n \n {{ \'tb.rulenode.add-period-between-msgs\' | translate }}\n \n \n tb.rulenode.period-value-key\n \n \n {{ \'tb.rulenode.period-value-key-required\' | translate }}\n \n \n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),_e=function(){function e(){}return e=h([t.NgModule({declarations:[Re,De,Oe,Ke,Be,He,Ge,je,Ue],imports:[r.CommonModule,a.SharedModule,ue],exports:[Re,De,Oe,Ke,Be,He,Ge,je,Ue]})],e)}(),ze=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.originatorSource=F,n.originatorSources=Object.keys(F),n.originatorSourceTranslationMap=w,n}return b(r,e),r.prototype.configForm=function(){return this.changeOriginatorConfigForm},r.prototype.onConfigurationSet=function(e){this.changeOriginatorConfigForm=this.fb.group({originatorSource:[e?e.originatorSource:null,[i.Validators.required]],relationsQuery:[e?e.relationsQuery:null,[]]})},r.prototype.validatorTriggers=function(){return["originatorSource"]},r.prototype.updateValidators=function(e){var t=this.changeOriginatorConfigForm.get("originatorSource").value;t&&t===F.RELATED?this.changeOriginatorConfigForm.get("relationsQuery").setValidators([i.Validators.required]):this.changeOriginatorConfigForm.get("relationsQuery").setValidators([]),this.changeOriginatorConfigForm.get("relationsQuery").updateValueAndValidity({emitEvent:e})},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),Qe=function(e){function r(t,r,n,a){var o=e.call(this,t)||this;return o.store=t,o.fb=r,o.nodeScriptTestService=n,o.translate=a,o}return b(r,e),r.prototype.configForm=function(){return this.scriptConfigForm},r.prototype.onConfigurationSet=function(e){this.scriptConfigForm=this.fb.group({jsScript:[e?e.jsScript:null,[i.Validators.required]]})},r.prototype.testScript=function(){var e=this,t=this.scriptConfigForm.get("jsScript").value;this.nodeScriptTestService.testNodeScript(t,"update",this.translate.instant("tb.rulenode.transformer"),"Transform",["msg","metadata","msgType"],this.ruleNodeId,"rulenode/transformation_node_script_fn").subscribe((function(t){t&&e.scriptConfigForm.get("jsScript").setValue(t)}))},r.prototype.onValidate=function(){this.jsFuncComponent.validateOnSubmit()},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder},{type:s.NodeScriptTestService},{type:n.TranslateService}]},h([t.ViewChild("jsFuncComponent",{static:!0}),C("design:type",a.JsFuncComponent)],r.prototype,"jsFuncComponent",void 0),r=h([t.Component({selector:"tb-transformation-node-script-config",template:'
\n \n \n \n
\n \n
\n
\n'}),C("design:paramtypes",[o.Store,i.FormBuilder,s.NodeScriptTestService,n.TranslateService])],r)}(a.RuleNodeConfigurationComponent),$e=function(e){function r(t,r){var n=e.call(this,t)||this;return n.store=t,n.fb=r,n.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"}],n}return b(r,e),r.prototype.configForm=function(){return this.toEmailConfigForm},r.prototype.onConfigurationSet=function(e){var t=this;this.toEmailConfigForm=this.fb.group({fromTemplate:[e?e.fromTemplate:null,[i.Validators.required]],toTemplate:[e?e.toTemplate:null,[i.Validators.required]],ccTemplate:[e?e.ccTemplate:null,[]],bccTemplate:[e?e.bccTemplate:null,[]],subjectTemplate:[e?e.subjectTemplate:null,[i.Validators.required]],mailBodyType:[e?e.mailBodyType:null],isHtmlTemplate:[e?e.isHtmlTemplate:null],bodyTemplate:[e?e.bodyTemplate:null,[i.Validators.required]]}),this.toEmailConfigForm.get("mailBodyType").valueChanges.pipe(f.startWith([null==e?void 0:e.subjectTemplate])).subscribe((function(e){"dynamic"===e?(t.toEmailConfigForm.get("isHtmlTemplate").patchValue("",{emitEvent:!1}),t.toEmailConfigForm.get("isHtmlTemplate").setValidators(i.Validators.required)):t.toEmailConfigForm.get("isHtmlTemplate").clearValidators(),t.toEmailConfigForm.get("isHtmlTemplate").updateValueAndValidity()}))},r.ctorParameters=function(){return[{type:o.Store},{type:i.FormBuilder}]},r=h([t.Component({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'}),C("design:paramtypes",[o.Store,i.FormBuilder])],r)}(a.RuleNodeConfigurationComponent),We=function(){function e(){}return e=h([t.NgModule({declarations:[ze,Qe,$e],imports:[r.CommonModule,a.SharedModule,ue],exports:[ze,Qe,$e]})],e)}(),Je=function(){function e(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","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.","start-interval":"Start Interval","end-interval":"End Interval","start-interval-time-unit":"Start Interval Time Unit","end-interval-time-unit":"End Interval Time Unit","fetch-mode":"Fetch mode","fetch-mode-hint":"If selected fetch mode 'ALL' you able to choose telemetry sampling order.","order-by":"Order by","order-by-hint":"Select to choose telemetry sampling order.",limit:"Limit","limit-hint":"Min limit value is 2, max - 1000. In case you want to fetch a single entry, select fetch mode 'FIRST' or 'LAST'.","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","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","notify-device":"Notify Device","notify-device-hint":"If the message arrives from the device, we will push it back to the device by default.","latest-timeseries":"Latest timeseries","timeseries-key":"Timeseries key","data-keys":"Message data","metadata-keys":"Message metadata","relations-query":"Relations query","device-relations-query":"Device relations query","max-relation-level":"Max relation level","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","attr-mapping":"Attributes mapping","source-attribute":"Source attribute","source-attribute-required":"Source attribute is required.","source-telemetry":"Source telemetry","source-telemetry-required":"Source telemetry is required.","target-attribute":"Target attribute","target-attribute-required":"Target attribute is required.","attr-mapping-required":"At least one attribute mapping should be specified.","fields-mapping":"Fields mapping","fields-mapping-required":"At least one field mapping should be specified.","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","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-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",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","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","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","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-interval-patterns":"Use interval patterns","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","use-dynamically-change-the-severity-of-alar":"Use dynamically change the severity of alarm","check-all-keys":"Check that all selected keys 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-pattern":"Start interval pattern","end-interval-pattern":"End interval pattern","start-interval-pattern-required":"Start interval pattern is required","end-interval-pattern-required":"End interval pattern 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":'You should press "enter" to complete field input.',"entity-details":"Select entity details:","entity-details-title":"Title","entity-details-country":"Country","entity-details-state":"State","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","add-to-metadata":"Add selected details to message metadata","add-to-metadata-hint":"If selected, adds the selected details keys to the message metadata instead of message data.","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":"You should 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-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 Latest telemetry with Timestamp","get-latest-value-with-ts-hint":'If selected, latest telemetry values will be added to the outbound message metadata with timestamp, e.g: "temp": "{"ts":1574329385897, "value":42}"',"use-redis-queue":"Use redis queue for message persistence","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.",round:"Decimals","round-range":"Decimals should be in a range from 0 to 15.","use-cache":"Use cache for latest value","tell-failure-if-delta-is-negative":"Tell Failure if delta is negative","add-period-between-msgs":"Add period between messages","period-value-key":"Period value key","period-key-required":"Period value key is required.","general-pattern-hint":'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.)'},"key-val":{key:"Key",value:"Value","remove-entry":"Remove entry","add-entry":"Add entry"},"mail-body-type":{"plain-text":"Plain Text",html:"HTML",dynamic:"Dynamic"}}},!0)}(e)}return e.ctorParameters=function(){return[{type:n.TranslateService}]},e=h([t.NgModule({declarations:[x],imports:[r.CommonModule,a.SharedModule],exports:[Ie,we,_e,We,x]}),C("design:paramtypes",[n.TranslateService])],e)}();e.RuleNodeCoreConfigModule=Je,e.ɵa=x,e.ɵb=Ie,e.ɵba=ve,e.ɵbb=Fe,e.ɵbc=xe,e.ɵbd=Te,e.ɵbe=qe,e.ɵbf=Se,e.ɵbg=ue,e.ɵbh=ae,e.ɵbi=oe,e.ɵbj=ie,e.ɵbk=le,e.ɵbl=se,e.ɵbm=me,e.ɵbn=we,e.ɵbo=ke,e.ɵbp=Ne,e.ɵbq=Ve,e.ɵbr=Ee,e.ɵbs=Ae,e.ɵbt=Le,e.ɵbu=Pe,e.ɵbv=Me,e.ɵbw=_e,e.ɵbx=Re,e.ɵby=De,e.ɵbz=Oe,e.ɵc=T,e.ɵca=Ke,e.ɵcb=Be,e.ɵcc=He,e.ɵcd=Ge,e.ɵce=je,e.ɵcf=Ue,e.ɵcg=We,e.ɵch=ze,e.ɵci=Qe,e.ɵcj=$e,e.ɵd=q,e.ɵe=S,e.ɵf=I,e.ɵg=k,e.ɵh=N,e.ɵi=V,e.ɵj=E,e.ɵk=A,e.ɵl=L,e.ɵm=P,e.ɵn=ee,e.ɵo=te,e.ɵp=re,e.ɵq=ne,e.ɵr=de,e.ɵs=pe,e.ɵt=ce,e.ɵu=fe,e.ɵv=ge,e.ɵw=ye,e.ɵx=be,e.ɵy=he,e.ɵz=Ce,Object.defineProperty(e,"__esModule",{value:!0})})); //# sourceMappingURL=rulenode-core-config.umd.min.js.map \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java new file mode 100644 index 0000000000..533bdff03f --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2021 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.metadata; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.kv.Aggregation; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.Mockito.mock; + +public class TbGetTelemetryNodeTest { + + TbGetTelemetryNode node; + + @Before + public void setUp() throws Exception { + node = mock(TbGetTelemetryNode.class); + willCallRealMethod().given(node).parseAggregationConfig(any()); + } + + @Test + public void givenAggregationAsString_whenParseAggregation_thenReturnEnum() { + //compatibility with old configs without "aggregation" parameter + assertThat(node.parseAggregationConfig(null), is(Aggregation.NONE)); + assertThat(node.parseAggregationConfig(""), is(Aggregation.NONE)); + + //common values + assertThat(node.parseAggregationConfig("MIN"), is(Aggregation.MIN)); + assertThat(node.parseAggregationConfig("MAX"), is(Aggregation.MAX)); + assertThat(node.parseAggregationConfig("AVG"), is(Aggregation.AVG)); + assertThat(node.parseAggregationConfig("SUM"), is(Aggregation.SUM)); + assertThat(node.parseAggregationConfig("COUNT"), is(Aggregation.COUNT)); + assertThat(node.parseAggregationConfig("NONE"), is(Aggregation.NONE)); + + //all possible values in future + for (Aggregation aggEnum : Aggregation.values()) { + assertThat(node.parseAggregationConfig(aggEnum.name()), is(aggEnum)); + } + } + + @Test(expected = IllegalArgumentException.class) + public void givenAggregationWhiteSpace_whenParseAggregation_thenException() { + node.parseAggregationConfig(" "); + } + + @Test(expected = IllegalArgumentException.class) + public void givenAggregationIncorrect_whenParseAggregation_thenException() { + node.parseAggregationConfig("TOP"); + } + +} 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 new file mode 100644 index 0000000000..52e37abec9 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbHttpClientTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2021 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.rest; + + +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willCallRealMethod; +import static org.mockito.Mockito.mock; + +public class TbHttpClientTest { + + EventLoopGroup eventLoop; + TbHttpClient client; + + @Before + public void setUp() throws Exception { + client = mock(TbHttpClient.class); + willCallRealMethod().given(client).getSharedOrCreateEventLoopGroup(any()); + } + + @After + public void tearDown() throws Exception { + if (eventLoop != null) { + eventLoop.shutdownGracefully(); + } + } + + @Test + public void givenSharedEventLoop_whenGetEventLoop_ThenReturnShared() { + eventLoop = mock(EventLoopGroup.class); + assertThat(client.getSharedOrCreateEventLoopGroup(eventLoop), is(eventLoop)); + } + + @Test + public void givenNull_whenGetEventLoop_ThenReturnShared() { + eventLoop = client.getSharedOrCreateEventLoopGroup(null); + assertThat(eventLoop, instanceOf(NioEventLoopGroup.class)); + } +} \ No newline at end of file diff --git a/tools/src/main/shell/client.keygen.sh b/tools/src/main/shell/client.keygen.sh index 981160a2a2..49cb2faaac 100755 --- a/tools/src/main/shell/client.keygen.sh +++ b/tools/src/main/shell/client.keygen.sh @@ -96,7 +96,7 @@ keytool -genkeypair -v \ -keypass $CLIENT_KEY_PASSWORD \ -storepass $CLIENT_KEYSTORE_PASSWORD \ -keyalg $CLIENT_KEY_ALG \ - -keysize $CLIENT_KEY_SIZE\ + -groupname $CLIENT_KEY_GROUP_NAME \ -validity 9999 \ -dname "CN=$DOMAIN_SUFFIX, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties index 4808f05b2f..239ec8dd48 100644 --- a/tools/src/main/shell/keygen.properties +++ b/tools/src/main/shell/keygen.properties @@ -16,8 +16,8 @@ DOMAIN_SUFFIX="$(hostname)" SUBJECT_ALTERNATIVE_NAMES="ip:127.0.0.1" -ORGANIZATIONAL_UNIT=Thingsboard -ORGANIZATION=Thingsboard +ORGANIZATIONAL_UNIT=ThingsBoard +ORGANIZATION=ThingsBoard CITY="San Francisco" STATE_OR_PROVINCE=CA TWO_LETTER_COUNTRY_CODE=US @@ -27,8 +27,8 @@ SERVER_KEY_PASSWORD=password SERVER_KEY_ALIAS="serveralias" SERVER_FILE_PREFIX="server" -SERVER_KEY_ALG="RSA" -SERVER_KEY_SIZE="2048" +SERVER_KEY_ALG="EC" +SERVER_KEY_GROUP_NAME="secp256r1" SERVER_KEYSTORE_DIR="/etc/thingsboard/conf" CLIENT_KEYSTORE_PASSWORD=password @@ -36,5 +36,5 @@ CLIENT_KEY_PASSWORD=password CLIENT_KEY_ALIAS="clientalias" CLIENT_FILE_PREFIX="client" -CLIENT_KEY_ALG="RSA" -CLIENT_KEY_SIZE="2048" +CLIENT_KEY_ALG="EC" +CLIENT_KEY_GROUP_NAME="secp256r1" diff --git a/tools/src/main/shell/server.keygen.sh b/tools/src/main/shell/server.keygen.sh index 259e7faec1..d5379ba873 100755 --- a/tools/src/main/shell/server.keygen.sh +++ b/tools/src/main/shell/server.keygen.sh @@ -105,7 +105,7 @@ keytool -genkeypair -v \ -keypass $SERVER_KEY_PASSWORD \ -storepass $SERVER_KEYSTORE_PASSWORD \ -keyalg $SERVER_KEY_ALG \ - -keysize $SERVER_KEY_SIZE \ + -groupname $SERVER_KEY_GROUP_NAME \ -validity 9999 \ $EXT diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 0f66f28efb..9aab46b824 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -122,6 +122,9 @@ transport: log: enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" max_length: "${TB_TRANSPORT_LOG_MAX_LENGTH:1024}" + stats: + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 8840cc8e8b..f35f4136e7 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -94,6 +94,9 @@ transport: log: enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" max_length: "${TB_TRANSPORT_LOG_MAX_LENGTH:1024}" + stats: + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 3398d82234..c4d2a71f94 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -147,6 +147,9 @@ transport: paging_transmission_window: "${LWM2M_PAGING_TRANSMISSION_WINDOW:10000}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" + stats: + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 2191508636..e0d0144d84 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -127,6 +127,9 @@ transport: log: enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" max_length: "${TB_TRANSPORT_LOG_MAX_LENGTH:1024}" + stats: + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" queue: diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 3453a7ace1..80bc257a8b 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -59,6 +59,9 @@ transport: log: enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" max_length: "${TB_TRANSPORT_LOG_MAX_LENGTH:1024}" + stats: + enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" + print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 90585f0ac0..a97e65b04c 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -79,7 +79,8 @@ "src/app/modules/home/components/widget/lib/maps/markers.scss", "node_modules/leaflet.markercluster/dist/MarkerCluster.css", "node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css", - "node_modules/prismjs/themes/prism.css" + "node_modules/prismjs/themes/prism.css", + "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css" ], "stylePreprocessorOptions": { "includePaths": [ @@ -92,8 +93,12 @@ "node_modules/systemjs/dist/system.js", "node_modules/marked/lib/marked.js", "node_modules/prismjs/prism.js", + "node_modules/prismjs/components/prism-css.min.js", "node_modules/prismjs/components/prism-bash.min.js", - "node_modules/prismjs/components/prism-json.min.js" + "node_modules/prismjs/components/prism-json.min.js", + "node_modules/prismjs/components/prism-javascript.min.js", + "node_modules/prismjs/components/prism-typescript.min.js", + "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js" ], "customWebpackConfig": { "path": "./extra-webpack.config.js" diff --git a/ui-ngx/package.json b/ui-ngx/package.json index fa5c9e904d..519d31b66c 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -8,7 +8,8 @@ "build:prod": "ng build --prod --vendor-chunk", "test": "ng test", "lint": "ng lint", - "e2e": "ng e2e" + "e2e": "ng e2e", + "prepare": "patch-package" }, "private": true, "dependencies": { @@ -42,7 +43,7 @@ "angular2-hotkeys": "^2.2.0", "canvas-gauges": "^2.1.7", "compass-sass-mixins": "^0.12.7", - "core-js": "^3.12.1", + "core-js": "^3.18.3", "date-fns": "^2.21.3", "flot": "git://github.com/thingsboard/flot.git#0.9-work", "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", @@ -72,7 +73,7 @@ "ngx-drag-drop": "^2.0.0", "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#master", "ngx-hm-carousel": "^2.0.0-rc.1", - "ngx-markdown": "^10.1.1", + "ngx-markdown": "^11.1.3", "ngx-sharebuttons": "^8.0.5", "ngx-translate-messageformat-compiler": "^4.9.0", "objectpath": "^2.0.0", @@ -135,10 +136,15 @@ "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "ngrx-store-freeze": "^0.2.4", + "patch-package": "^6.4.7", + "postinstall-prepare": "^2.0.0", "protractor": "~7.0.0", "ts-node": "^9.0.0", "tslint": "~6.1.3", "typescript": "~4.0.3", "webpack": "^4.46.0" + }, + "resolutions": { + "lodash": "~4.17.21" } } diff --git a/ui-ngx/patches/leaflet+1.7.1.patch b/ui-ngx/patches/leaflet+1.7.1.patch new file mode 100644 index 0000000000..b7688d4f33 --- /dev/null +++ b/ui-ngx/patches/leaflet+1.7.1.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/leaflet/dist/leaflet-src.js b/node_modules/leaflet/dist/leaflet-src.js +index 9acc7da..6c9acf8 100644 +--- a/node_modules/leaflet/dist/leaflet-src.js ++++ b/node_modules/leaflet/dist/leaflet-src.js +@@ -2050,6 +2050,8 @@ + obj.removeEventListener(POINTER_CANCEL, handler, false); + } + ++ delete obj['_leaflet_' + type + id]; ++ + return this; + } + diff --git a/ui-ngx/src/app/core/api/alias-controller.ts b/ui-ngx/src/app/core/api/alias-controller.ts index 5f7474a31b..68e12411c1 100644 --- a/ui-ngx/src/app/core/api/alias-controller.ts +++ b/ui-ngx/src/app/core/api/alias-controller.ts @@ -323,6 +323,9 @@ export class AliasController implements IAliasController { } resolveDatasources(datasources: Array, singleEntity?: boolean): Observable> { + if (!datasources || !datasources.length) { + return of([]); + } const toResolve = singleEntity ? [datasources[0]] : datasources; const observables = new Array>(); toResolve.forEach((datasource) => { diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 300998548c..9bc621fe22 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -25,7 +25,7 @@ import { SubscriptionTimewindow } from '@shared/models/time/time.models'; import { UtilsService } from '@core/services/utils.service'; -import { deepClone, isNumeric } from '@core/utils'; +import { deepClone, isNumber, isNumeric } from '@core/utils'; import Timeout = NodeJS.Timeout; export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; @@ -92,20 +92,36 @@ declare type AggFunction = (aggData: AggData, value?: any) => void; const avg: AggFunction = (aggData: AggData, value?: any) => { aggData.count++; - aggData.sum += value; - aggData.aggValue = aggData.sum / aggData.count; + if (isNumber(value)) { + aggData.sum += value; + aggData.aggValue = aggData.sum / aggData.count; + } else { + aggData.aggValue = value; + } }; const min: AggFunction = (aggData: AggData, value?: any) => { - aggData.aggValue = Math.min(aggData.aggValue, value); + if (isNumber(value)) { + aggData.aggValue = Math.min(aggData.aggValue, value); + } else { + aggData.aggValue = value; + } }; const max: AggFunction = (aggData: AggData, value?: any) => { - aggData.aggValue = Math.max(aggData.aggValue, value); + if (isNumber(value)) { + aggData.aggValue = Math.max(aggData.aggValue, value); + } else { + aggData.aggValue = value; + } }; const sum: AggFunction = (aggData: AggData, value?: any) => { - aggData.aggValue = aggData.aggValue + value; + if (isNumber(value)) { + aggData.aggValue = aggData.aggValue + value; + } else { + aggData.aggValue = value; + } }; const count: AggFunction = (aggData: AggData) => { @@ -409,7 +425,7 @@ export class DataAggregator { } private convertValue(val: string): any { - if (!this.noAggregation || val && isNumeric(val) && Number(val).toString() === val) { + if (val && isNumeric(val) && (!this.noAggregation || this.noAggregation && Number(val).toString() === val)) { return Number(val); } return val; 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 4f46212231..d053efb601 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -54,6 +54,7 @@ import { PageData } from '@shared/models/page/page-data'; import { TranslateService } from '@ngx-translate/core'; import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; +import { PopoverPlacement } from '@shared/components/popover.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -89,6 +90,10 @@ export interface WidgetActionsApi { getActiveEntityInfo: () => SubscriptionEntityInfo; openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void; + openDashboardStateInPopover: ($event: Event, targetDashboardStateId: string, params?: StateParams, + hideDashboardToolbar?: boolean, preferredPlacement?: PopoverPlacement, + hideOnClickOutside?: boolean, popoverWidth?: string, + popoverHeight?: string, popoverStyle?: { [klass: string]: any }) => void; } export interface AliasInfo { @@ -230,6 +235,7 @@ export interface WidgetSubscriptionOptions { stateData?: boolean; alarmSource?: Datasource; datasources?: Array; + datasourcesOptional?: boolean; hasDataPageLink?: boolean; singleEntity?: boolean; warnOnPageDataOverflow?: boolean; @@ -277,6 +283,7 @@ export interface IWidgetSubscription { hiddenData?: Array<{data: DataSet}>; timeWindowConfig?: Timewindow; timeWindow?: WidgetTimewindow; + widgetTimewindowChanged$: Observable; comparisonEnabled?: boolean; comparisonTimeWindow?: WidgetTimewindow; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 5481af9255..21f95a9158 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -67,7 +67,7 @@ import { KeyFilter, updateDatasourceFromEntityInfo } from '@shared/models/query/query.models'; -import { filter, map, switchMap, takeUntil } from 'rxjs/operators'; +import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; @@ -106,6 +106,7 @@ export class WidgetSubscription implements IWidgetSubscription { caulculateLegendData: boolean; displayLegend: boolean; stateData: boolean; + datasourcesOptional: boolean; decimals: number; units: string; comparisonEnabled: boolean; @@ -139,6 +140,11 @@ export class WidgetSubscription implements IWidgetSubscription { executingSubjects: Array>; subscribed = false; + widgetTimewindowChangedSubject: Subject = new ReplaySubject(); + + widgetTimewindowChanged$ = this.widgetTimewindowChangedSubject.asObservable().pipe( + distinct() + ); constructor(subscriptionContext: WidgetSubscriptionContext, public options: WidgetSubscriptionOptions) { const subscriptionSubject = new ReplaySubject(); @@ -176,6 +182,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {}); this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {}); this.alarmSource = options.alarmSource; + this.datasourcesOptional = options.datasourcesOptional; this.alarmDataListener = null; this.alarms = emptyPageData(); this.originalTimewindow = null; @@ -207,6 +214,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {}); this.configuredDatasources = this.ctx.utils.validateDatasources(options.datasources); + this.datasourcesOptional = options.datasourcesOptional; this.entityDataListeners = []; this.hasDataPageLink = options.hasDataPageLink; this.singleEntity = options.singleEntity; @@ -387,7 +395,7 @@ export class WidgetSubscription implements IWidgetSubscription { } private prepareDataSubscriptions(): Observable { - if (this.hasDataPageLink) { + if (this.hasDataPageLink || !this.configuredDatasources || !this.configuredDatasources.length) { this.hasResolvedData = true; this.notifyDataLoaded(); return of(null); @@ -443,7 +451,7 @@ export class WidgetSubscription implements IWidgetSubscription { } }); this.configureLoadedData(); - this.hasResolvedData = this.datasources.length > 0; + this.hasResolvedData = this.datasources.length > 0 || this.datasourcesOptional; this.updateDataTimewindow(); this.notifyDataLoaded(); this.onDataUpdated(true); @@ -805,6 +813,7 @@ export class WidgetSubscription implements IWidgetSubscription { update(isTimewindowTypeChanged = false) { if (this.type !== widgetType.rpc) { + this.widgetTimewindowChangedSubject.next(this.timeWindowConfig); if (this.type === widgetType.alarm) { this.updateAlarmDataSubscription(); } else { @@ -1118,6 +1127,7 @@ export class WidgetSubscription implements IWidgetSubscription { destroy(): void { this.unsubscribe(); + this.widgetTimewindowChangedSubject.complete(); for (const cafId of Object.keys(this.cafs)) { if (this.cafs[cafId]) { this.cafs[cafId](); diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 6665beb3b3..3346d5ce83 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -83,7 +83,12 @@ import { import { alarmFields } from '@shared/models/alarm.models'; import { OtaPackageService } from '@core/http/ota-package.service'; import { EdgeService } from '@core/http/edge.service'; -import { Edge, EdgeEvent, EdgeEventType } from '@shared/models/edge.models'; +import { + Edge, + EdgeEvent, + EdgeEventType, + bodyContentEdgeEventActionTypes +} from '@shared/models/edge.models'; import { RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models'; import { WidgetService } from '@core/http/widget.service'; import { DeviceProfileService } from '@core/http/device-profile.service'; @@ -1402,7 +1407,11 @@ export class EntityService { case EdgeEventType.ASSET: case EdgeEventType.DEVICE: case EdgeEventType.ENTITY_VIEW: - entityObservable = this.getEntity(entityType, entityId, { ignoreLoading: true, ignoreErrors: true }); + if (bodyContentEdgeEventActionTypes.includes(entity.action)) { + entityObservable = of(entity.body); + } else { + entityObservable = this.getEntity(entityType, entityId, { ignoreLoading: true, ignoreErrors: true }); + } break; case EdgeEventType.RULE_CHAIN_METADATA: entityObservable = this.ruleChainService.getRuleChainMetadata(entityId); diff --git a/ui-ngx/src/app/core/http/ui-settings.service.ts b/ui-ngx/src/app/core/http/ui-settings.service.ts new file mode 100644 index 0000000000..e86ecb53e7 --- /dev/null +++ b/ui-ngx/src/app/core/http/ui-settings.service.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2021 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 { HttpClient } from '@angular/common/http'; +import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; +import { Observable } from 'rxjs'; +import { publishReplay, refCount } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class UiSettingsService { + + private helpBaseUrlObservable: Observable; + + constructor( + private http: HttpClient + ) { } + + public getHelpBaseUrl(): Observable { + if (!this.helpBaseUrlObservable) { + this.helpBaseUrlObservable = this.http.get('/api/uiSettings/helpBaseUrl', {responseType: 'text', ...defaultHttpOptions(true)}).pipe( + publishReplay(1), + refCount() + ); + } + return this.helpBaseUrlObservable; + } +} diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 41bf604177..d6de011133 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -68,6 +68,10 @@ export class WidgetService { ); } + public getWidgetScopeVariables(): string[] { + return ['tinycolor', 'cssjs', 'moment', '$', 'jQuery']; + } + public getAllWidgetsBundles(config?: RequestConfig): Observable> { return this.loadWidgetsBundleCache(config).pipe( map(() => this.allWidgetsBundles) @@ -311,5 +315,4 @@ export class WidgetService { this.loadWidgetsBundleCacheSubject = undefined; this.widgetTypeInfosCache.clear(); } - } 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 f840c89c26..31cbeeb3b7 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 @@ -23,10 +23,12 @@ import { NgModule, NgModuleRef, OnDestroy, - Type + Type, + ɵresetCompiledComponents } from '@angular/core'; -import { Observable, ReplaySubject } from 'rxjs'; +import { from, Observable } from 'rxjs'; import { CommonModule } from '@angular/common'; +import { catchError, map, mergeMap } from 'rxjs/operators'; @NgModule() export abstract class DynamicComponentModule implements OnDestroy { @@ -55,11 +57,12 @@ export class DynamicComponentFactoryService { public createDynamicComponentFactory( componentType: Type, template: string, - modules?: Type[]): Observable> { - const dymamicComponentFactorySubject = new ReplaySubject>(); - import('@angular/compiler').then( - () => { - const comp = this.createDynamicComponent(componentType, template); + modules?: Type[], + preserveWhitespaces?: boolean, + compileAttempt = 1): Observable> { + return from(import('@angular/compiler')).pipe( + mergeMap(() => { + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); let moduleImports: Type[] = [CommonModule]; if (modules) { moduleImports = [...moduleImports, ...modules]; @@ -69,29 +72,33 @@ export class DynamicComponentFactoryService { declarations: [comp], imports: moduleImports })(class DynamicComponentInstanceModule extends DynamicComponentModule {}); - try { - this.compiler.compileModuleAsync(dynamicComponentInstanceModule).then( - (module) => { - const moduleRef = module.create(this.injector); - const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); - this.dynamicComponentModulesMap.set(factory, { - moduleRef, - moduleType: module.moduleType - }); - dymamicComponentFactorySubject.next(factory); - dymamicComponentFactorySubject.complete(); + return from(this.compiler.compileModuleAsync(dynamicComponentInstanceModule)).pipe( + map((module) => { + let moduleRef: NgModuleRef; + try { + moduleRef = module.create(this.injector); + } catch (e) { + this.compiler.clearCacheFor(module.moduleType); + throw e; } - ).catch( - (e) => { - dymamicComponentFactorySubject.error(e); + const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(comp); + this.dynamicComponentModulesMap.set(factory, { + moduleRef, + moduleType: module.moduleType + }); + return factory; + }), + catchError((error) => { + if (compileAttempt === 1) { + ɵresetCompiledComponents(); + return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt); + } else { + throw error; } - ); - } catch (e) { - dymamicComponentFactorySubject.error(e); - } - } + }) + ); + }) ); - return dymamicComponentFactorySubject.asObservable(); } public destroyDynamicComponentFactory(factory: ComponentFactory) { @@ -103,10 +110,11 @@ export class DynamicComponentFactoryService { } } - private createDynamicComponent(componentType: Type, template: string): Type { + private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean): Type { // noinspection AngularMissingOrInvalidDeclarationInModule return Component({ - template + template, + preserveWhitespaces })(componentType); } diff --git a/ui-ngx/src/app/core/services/help.service.ts b/ui-ngx/src/app/core/services/help.service.ts new file mode 100644 index 0000000000..df8b14d71c --- /dev/null +++ b/ui-ngx/src/app/core/services/help.service.ts @@ -0,0 +1,134 @@ +/// +/// Copyright © 2016-2021 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 { HttpClient } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable, of } from 'rxjs'; +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; +import { helpBaseUrl as siteBaseUrl } from '@shared/models/constants'; +import { UiSettingsService } from '@core/http/ui-settings.service'; + +const localHelpBaseUrl = '/assets'; + +const NOT_FOUND_CONTENT: HelpData = { + content: '## Not found', + helpBaseUrl: localHelpBaseUrl +}; + +@Injectable({ + providedIn: 'root' +}) +export class HelpService { + + private siteBaseUrl = siteBaseUrl; + private helpCache: {[lang: string]: {[key: string]: string}} = {}; + + constructor( + private translate: TranslateService, + private http: HttpClient, + private uiSettingsService: UiSettingsService + ) {} + + getHelpContent(key: string): Observable { + const lang = this.translate.currentLang; + if (this.helpCache[lang] && this.helpCache[lang][key]) { + return of(this.helpCache[lang][key]); + } else { + return this.loadHelpContent(lang, key).pipe( + catchError(() => { + const defaultLang = this.translate.getDefaultLang(); + if (lang !== defaultLang) { + return this.loadHelpContent(defaultLang, key).pipe( + catchError(() => { + return of(NOT_FOUND_CONTENT); + }) + ); + } else { + return of(NOT_FOUND_CONTENT); + } + }), + mergeMap((content) => { + return this.processIncludes(this.processVariables(content)); + }), + tap((content) => { + let langContent = this.helpCache[lang]; + if (!langContent) { + langContent = {}; + this.helpCache[lang] = langContent; + } + langContent[key] = content; + }) + ); + } + } + + private loadHelpContent(lang: string, key: string): Observable { + return this.uiSettingsService.getHelpBaseUrl().pipe( + mergeMap((helpBaseUrl) => { + return this.loadHelpContentFromBaseUrl(helpBaseUrl, lang, key).pipe( + catchError((e) => { + if (localHelpBaseUrl !== helpBaseUrl) { + return this.loadHelpContentFromBaseUrl(localHelpBaseUrl, lang, key); + } else { + throw e; + } + }) + ); + }) + ); + } + + private loadHelpContentFromBaseUrl(helpBaseUrl: string, lang: string, key: string): Observable { + return this.http.get(`${helpBaseUrl}/help/${lang}/${key}.md`, {responseType: 'text'} ).pipe( + map((content) => { + return { + content, + helpBaseUrl + }; + }) + ); + } + + private processVariables(helpData: HelpData): string { + const baseUrlReg = /\${siteBaseUrl}/g; + helpData.content = helpData.content.replace(baseUrlReg, this.siteBaseUrl); + const helpBaseUrlReg = /\${helpBaseUrl}/g; + return helpData.content.replace(helpBaseUrlReg, helpData.helpBaseUrl); + } + + private processIncludes(content: string): Observable { + const includesRule = /{% include (.*) %}/; + const match = includesRule.exec(content); + if (match) { + const key = match[1]; + return this.getHelpContent(key).pipe( + mergeMap((include) => { + content = content.replace(match[0], include); + return this.processIncludes(content); + }) + ); + } else { + return of(content); + } + } + +} + +interface HelpData { + content: string; + helpBaseUrl: string; +} diff --git a/ui-ngx/src/app/core/services/script/node-script-test.service.ts b/ui-ngx/src/app/core/services/script/node-script-test.service.ts index 63de227ab7..47b83e96dc 100644 --- a/ui-ngx/src/app/core/services/script/node-script-test.service.ts +++ b/ui-ngx/src/app/core/services/script/node-script-test.service.ts @@ -35,7 +35,7 @@ export class NodeScriptTestService { } testNodeScript(script: string, scriptType: string, functionTitle: string, - functionName: string, argNames: string[], ruleNodeId: string): Observable { + functionName: string, argNames: string[], ruleNodeId: string, helpId?: string): Observable { if (ruleNodeId) { return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( switchMap((debugIn) => { @@ -52,18 +52,18 @@ export class NodeScriptTestService { msgType = debugIn.msgType; } return this.openTestScriptDialog(script, scriptType, functionTitle, - functionName, argNames, msg, metadata, msgType); + functionName, argNames, msg, metadata, msgType, helpId); }) ); } else { return this.openTestScriptDialog(script, scriptType, functionTitle, - functionName, argNames); + functionName, argNames, null, null, null, helpId); } } private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string, argNames: string[], - msg?: any, metadata?: {[key: string]: string}, msgType?: string): Observable { + msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string): Observable { if (!msg) { msg = { temperature: 22.4, @@ -94,7 +94,8 @@ export class NodeScriptTestService { functionName, script, scriptType, - argNames + argNames, + helpId } }).afterClosed(); } diff --git a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts index a56c19c281..9fbfd4c261 100644 --- a/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.ts @@ -17,7 +17,7 @@ import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, of } from 'rxjs'; -import { catchError, debounceTime, distinctUntilChanged, map, startWith, switchMap, tap } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; @@ -26,6 +26,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { EntityInfo } from '@shared/models/entity.models'; import { EntityFilter } from '@shared/models/query/query.models'; import { EntityService } from '@core/http/entity.service'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ selector: 'tb-aliases-entity-autocomplete', @@ -98,10 +99,10 @@ export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, } this.updateView(modelValue); }), - startWith(''), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), distinctUntilChanged(), - switchMap(name => this.fetchEntityInfos(name)) + switchMap(name => this.fetchEntityInfos(name)), + share() ); } @@ -114,12 +115,12 @@ export class AliasesEntityAutocompleteComponent implements ControlValueAccessor, writeValue(value: EntityInfo | null): void { this.searchText = ''; - if (value != null) { + if (isDefinedAndNotNull(value)) { this.modelValue = value; this.selectEntityInfoFormGroup.get('entityInfo').patchValue(value, {emitEvent: true}); } else { this.modelValue = null; - this.selectEntityInfoFormGroup.get('entityInfo').patchValue(null, {emitEvent: true}); + this.selectEntityInfoFormGroup.get('entityInfo').patchValue(null, {emitEvent: false}); } } 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 5694cc5c77..bcb9edd07e 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 @@ -198,6 +198,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI attributeScopeChanged(attributeScope: TelemetryType) { this.attributeScope = attributeScope; this.mode = 'default'; + this.paginator.pageIndex = 0; this.updateData(true); } @@ -447,7 +448,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI if (this.mode === 'widget') { this.widgetsList = []; this.widgetsListCache = []; - this.widgetsCarouselIndex = 0; + this.widgetsCarouselIndex = 0; if (widgetsBundle) { this.widgetsLoaded = false; const bundleAlias = widgetsBundle.alias; 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 b381a34bfd..a97004d798 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 @@ -15,7 +15,7 @@ limitations under the License. --> -
+ [widgetEditMode]="widgetEditMode" + [parentDashboard]="parentDashboard"> + [widgetEditMode]="widgetEditMode" + [parentDashboard]="parentDashboard"> - -
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts index 2ea1fc1cbc..92549206a2 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts @@ -78,6 +78,9 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo @Input() widgetEditMode: boolean; + @Input() + parentDashboard?: IDashboardComponent = null; + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; private rxSubscriptions = new Array(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts index 72c242c308..8e2321cbc3 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts @@ -133,6 +133,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo @Input() dashboardTimewindow: Timewindow; + @Input() + parentDashboard?: IDashboardComponent = null; + dashboardTimewindowChangedSubject: Subject = new ReplaySubject(); dashboardTimewindowChanged = this.dashboardTimewindowChangedSubject.asObservable().pipe( @@ -187,6 +190,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo } ngOnInit(): void { + this.dashboardWidgets.parentDashboard = this.parentDashboard; if (!this.dashboardTimewindow) { this.dashboardTimewindow = this.timeService.defaultTimewindow(); } @@ -238,6 +242,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo if (this.breakpointObserverSubscription) { this.breakpointObserverSubscription.unsubscribe(); } + this.dashboardTimewindowChangedSubject.complete(); this.gridster = null; } diff --git a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts index 517415a68a..38c3317ff0 100644 --- a/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/edge/edge-downlink-table-config.ts @@ -47,7 +47,7 @@ import { AttributeService } from '@core/http/attribute.service'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { EdgeDownlinkTableHeaderComponent } from '@home/components/edge/edge-downlink-table-header.component'; import { EdgeService } from '@core/http/edge.service'; -import { map } from 'rxjs/operators'; +import { concatMap, map } from 'rxjs/operators'; import { EntityService } from "@core/http/entity.service"; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -55,7 +55,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions' export class EdgeDownlinkTableConfig extends EntityTableConfig { - queueStartTs: number; + private queueStartTs: number; constructor(private attributeService: AttributeService, private datePipe: DatePipe, @@ -85,19 +85,14 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig> { - this.loadEdgeInfo(); - return this.edgeService.getEdgeEvents(this.entityId, pageLink); - } - - loadEdgeInfo(): void { - this.attributeService.getEntityAttributes(this.entityId, AttributeScope.SERVER_SCOPE, ['queueStartTs']) - .subscribe( - attributes => this.onUpdate(attributes) - ); + private fetchEvents(pageLink: TimePageLink): Observable> { + return this.attributeService.getEntityAttributes(this.entityId, AttributeScope.SERVER_SCOPE, ['queueStartTs']).pipe( + map((attributes) => this.onUpdate(attributes)), + concatMap(() => this.edgeService.getEdgeEvents(this.entityId, pageLink)) + ); } - onUpdate(attributes): void { + private onUpdate(attributes: any): void { this.queueStartTs = 0; let edge = attributes.reduce(function (map, attribute) { map[attribute.key] = attribute; @@ -108,7 +103,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig('createdTime', 'event.event-time', this.datePipe, '120px'), @@ -117,7 +112,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig('action', 'edge.event-action', '25%', entity => this.translate.instant(edgeEventActionTypeTranslations.get(entity.action)), entity => ({}), false), new EntityTableColumn('entityId', 'edge.entity-id', '40%', - (entity) => entity.entityId, entity => ({}), false), + (entity) => entity.entityId ? entity.entityId : '', () => ({}), false), new EntityTableColumn('status', 'event.status', '10%', (entity) => this.updateEdgeEventStatus(entity.createdTime), entity => ({ @@ -143,7 +138,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig this.queueStartTs; } - isEdgeEventHasData(entity: EdgeEvent): boolean { + private isEdgeEventHasData(entity: EdgeEvent): boolean { return !(entity.type === EdgeEventType.ADMIN_SETTINGS || entity.action === EdgeEventActionType.DELETED); } - prepareEdgeEventContent(entity: EdgeEvent): Observable { + private prepareEdgeEventContent(entity: EdgeEvent): Observable { return this.entityService.getEdgeEventContent(entity).pipe( map((result) => JSON.stringify(result)) ); } - showEdgeEventContent($event: MouseEvent, content: string, title: string): void { + private showEdgeEventContent($event: MouseEvent, content: string, title: string): void { if ($event) { $event.stopPropagation(); } @@ -181,7 +176,7 @@ export class EdgeDownlinkTableConfig extends EntityTableConfig - + {{ column.title | translate}} - + diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 49d8e87e44..9bf6912c9f 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -292,14 +292,14 @@ export class EventTableConfig extends EntityTableConfig { case EventType.ERROR: this.filterColumns.push( {key: 'method', title: 'event.method'}, - {key: 'error', title: 'event.error'} + {key: 'errorStr', title: 'event.error'} ); break; case EventType.LC_EVENT: this.filterColumns.push( {key: 'event', title: 'event.event'}, {key: 'status', title: 'event.status'}, - {key: 'error', title: 'event.error'} + {key: 'errorStr', title: 'event.error'} ); break; case EventType.STATS: @@ -319,7 +319,7 @@ export class EventTableConfig extends EntityTableConfig { {key: 'dataSearch', title: 'event.data'}, {key: 'metadataSearch', title: 'event.metadata'}, {key: 'isError', title: 'event.error'}, - {key: 'error', title: 'event.error'} + {key: 'errorStr', title: 'event.error'} ); break; } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html index 6ceb5b1844..cce002a7b2 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html @@ -43,7 +43,7 @@
{{ keyFilterControl.value.key.key }}
-
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) }}
+
{{ entityKeyTypeTranslations.get(keyFilterControl.value.key.type) | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html index 6aa835cf84..53351918e2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html @@ -94,8 +94,10 @@ [(ngModel)]="action.customFunction" (ngModelChange)="notifyActionUpdated()" [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel']" + [disableUndefinedCheck]="true" [validationArgs]="[]" - [editorCompleter]="customPrettyActionEditorCompleter"> + [editorCompleter]="customPrettyActionEditorCompleter" + helpId="widget/action/custom_pretty_action_fn"> diff --git a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html index cdb4615068..90feadcc44 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html @@ -35,7 +35,9 @@ formControlName="getLocationFunction" functionName="getLocation" [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" + [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_get_location_fn" >
@@ -43,7 +45,9 @@ formControlName="getPhoneNumberFunction" functionName="getPhoneNumber" [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" + [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_get_phone_number_fn" > @@ -62,7 +68,9 @@ formControlName="processQrCodeFunction" functionName="processQrCode" [functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" + [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_process_qr_code_fn" > @@ -70,7 +78,9 @@ formControlName="processLocationFunction" functionName="processLocation" [functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" + [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_process_location_fn" > @@ -89,12 +101,16 @@ formControlName="handleEmptyResultFunction" functionName="handleEmptyResult" [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" + [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_handle_empty_result_fn" > diff --git a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts index 966268faa3..75c42e7375 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts @@ -14,26 +14,29 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { Component, forwardRef, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { deepClone } from '@core/utils'; import { WidgetMobileActionDescriptor, - WidgetMobileActionType, widgetMobileActionTypeTranslationMap + WidgetMobileActionType, + widgetMobileActionTypeTranslationMap } from '@shared/models/widget.models'; import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; import { JsFuncComponent } from '@shared/components/js-func.component'; import { - getDefaultGetLocationFunction, getDefaultGetPhoneNumberFunction, + getDefaultGetLocationFunction, + getDefaultGetPhoneNumberFunction, getDefaultHandleEmptyResultFunction, getDefaultHandleErrorFunction, getDefaultProcessImageFunction, - getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, + getDefaultProcessLaunchResultFunction, + getDefaultProcessLocationFunction, getDefaultProcessQrCodeFunction } from '@home/components/widget/action/mobile-action-editor.models'; +import { WidgetService } from '@core/http/widget.service'; @Component({ selector: 'tb-mobile-action-editor', @@ -58,6 +61,8 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit mobileActionFormGroup: FormGroup; mobileActionTypeFormGroup: FormGroup; + functionScopeVariables: string[]; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -73,7 +78,9 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit private propagateChange = (v: any) => { }; constructor(private store: Store, - private fb: FormBuilder) { + private fb: FormBuilder, + private widgetService: WidgetService) { + this.functionScopeVariables = this.widgetService.getWidgetScopeVariables(); } registerOnChange(fn: any): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts index ea32b812be..14ca89288e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts @@ -111,12 +111,6 @@ const processLocationFunction = 'showLocationDialog(\'Location\', latitude, longitude);\n' + '// saveEntityLocationAttributes(\'latitude\', \'longitude\', latitude, longitude);\n' + '\n' + - 'function showImageDialog(title, imageUrl) {\n' + - ' setTimeout(function() {\n' + - ' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' + - ' }, 100);\n' + - '}\n' + - '\n' + 'function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {\n' + ' if (entityId) {\n' + ' let attributes = [\n' + diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index 2c7b826200..2cf9b427ec 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | translate }}

@@ -54,6 +54,18 @@ + + {{ 'widget-config.show-hide-action-using-function' | translate }} + + widget-config.action-type @@ -134,45 +146,85 @@ - - {{ 'widget-action.open-in-separate-dialog' | translate }} - -
- - widget-action.dialog-title - - - - {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} - - - widget-action.dialog-width - - - {{ 'widget-action.dialog-size-range-error' | translate }} - - - {{ 'widget-action.dialog-size-range-error' | translate }} - - - - widget-action.dialog-height - - - {{ 'widget-action.dialog-size-range-error' | translate }} - - - {{ 'widget-action.dialog-size-range-error' | translate }} - - + + widget-action.state-display-type + + + {{ stateDisplayTypeName(displayType) }} + + + +
+ + + widget-action.dialog-title + + + + {{ 'widget-action.dialog-hide-dashboard-toolbar' | translate }} + + + widget-action.dialog-width + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + + widget-action.dialog-height + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + {{ 'widget-action.dialog-size-range-error' | translate }} + + + + + + widget-action.popover-preferred-placement + + + {{ popoverPlacementName(placement) }} + + + + + {{ 'widget-action.popover-hide-on-click-outside' | translate }} + + + {{ 'widget-action.popover-hide-dashboard-toolbar' | translate }} + + + widget-action.popover-width + + + + widget-action.popover-height + + + + + +
@@ -197,7 +249,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index 8930456636..345e9b5877 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts @@ -28,7 +28,7 @@ import { ValidatorFn, Validators } from '@angular/forms'; -import { Observable, of } from 'rxjs'; +import { Observable, of, Subscription } from 'rxjs'; import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { @@ -38,7 +38,11 @@ import { WidgetActionsData } from '@home/components/widget/action/manage-widget-actions.component.models'; import { UtilsService } from '@core/services/utils.service'; -import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; +import { + WidgetActionSource, + WidgetActionType, + widgetActionTypeTranslationMap +} from '@shared/models/widget.models'; import { map, mergeMap, startWith, tap } from 'rxjs/operators'; import { DashboardService } from '@core/http/dashboard.service'; import { Dashboard } from '@shared/models/dashboard.models'; @@ -47,6 +51,9 @@ import { CustomActionEditorCompleter } from '@home/components/widget/action/cust import { isDefinedAndNotNull } from '@core/utils'; import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component'; import { widgetType } from '@shared/models/widget.models'; +import { WidgetService } from '@core/http/widget.service'; +import { TranslateService } from '@ngx-translate/core'; +import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models'; export interface WidgetActionDialogData { isAdd: boolean; @@ -56,6 +63,18 @@ export interface WidgetActionDialogData { widgetType: widgetType; } +const stateDisplayTypes = ['normal', 'separateDialog', 'popover'] as const; +type stateDisplayTypeTuple = typeof stateDisplayTypes; +export type stateDisplayType = stateDisplayTypeTuple[number]; + +const stateDisplayTypesTranslations = new Map( + [ + ['normal', 'widget-action.open-normal'], + ['separateDialog', 'widget-action.open-in-separate-dialog'], + ['popover', 'widget-action.open-in-popover'], + ] +); + @Component({ selector: 'tb-widget-action-dialog', templateUrl: './widget-action-dialog.component.html', @@ -71,6 +90,8 @@ export class WidgetActionDialogComponent extends DialogComponent, protected router: Router, private utils: UtilsService, private dashboardService: DashboardService, private dashboardUtils: DashboardUtilsService, + private widgetService: WidgetService, + private translate: TranslateService, @Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, public dialogRef: MatDialogRef, @@ -109,6 +137,7 @@ export class WidgetActionDialogComponent extends DialogComponent { this.updateActionTypeFormGroup(type); }); this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { this.widgetActionFormGroup.get('name').updateValueAndValidity(); + this.updateShowWidgetActionForm(); + }); + this.widgetActionFormGroup.get('useShowWidgetActionFunction').valueChanges.subscribe(() => { + this.updateShowWidgetActionForm(); }); } + displayShowWidgetActionForm(): boolean { + return !!this.data.actionsData.actionSources[this.widgetActionFormGroup.get('actionSourceId').value]?.hasShowCondition; + } + + getWidgetActionFunctionHelpId(): string | undefined { + const actionSourceId = this.widgetActionFormGroup.get('actionSourceId').value; + if (actionSourceId === 'headerButton') { + return 'widget/action/show_widget_action_header_fn'; + } else if (actionSourceId === 'actionCellButton') { + return 'widget/action/show_widget_action_cell_fn'; + } + return undefined; + } + + private updateShowWidgetActionForm() { + const actionSourceId = this.widgetActionFormGroup.get('actionSourceId').value; + const useShowWidgetActionFunction = this.widgetActionFormGroup.get('useShowWidgetActionFunction').value; + if (!!this.data.actionsData.actionSources[actionSourceId]?.hasShowCondition && useShowWidgetActionFunction) { + this.widgetActionFormGroup.get('showWidgetActionFunction').setValidators([Validators.required]); + } else { + this.widgetActionFormGroup.get('showWidgetActionFunction').clearValidators(); + } + this.widgetActionFormGroup.get('showWidgetActionFunction').updateValueAndValidity(); + } + private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { + this.actionTypeFormGroupSubscriptions.forEach(s => s.unsubscribe()); + this.actionTypeFormGroupSubscriptions.length = 0; this.actionTypeFormGroup = this.fb.group({}); if (type) { switch (type) { @@ -163,25 +228,16 @@ export class WidgetActionDialogComponent extends DialogComponent { + this.updateStateDisplayTypeFormGroup(displayTypeValue); + }) ); } this.actionTypeFormGroup.addControl( @@ -213,6 +269,76 @@ export class WidgetActionDialogComponent extends DialogComponent - +

{{ 'datakey.configuration' | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index fbc62010da..457bbdd777 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -67,8 +67,10 @@
@@ -78,8 +80,10 @@