diff --git a/application/pom.xml b/application/pom.xml index d21dc7bb2e..405f3b4d7d 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard application @@ -250,8 +250,8 @@ grpc-stub - io.springfox - springfox-swagger2 + org.thingsboard + springfox-boot-starter com.sun.winsw @@ -324,10 +324,6 @@ org.javadelight delight-nashorn-sandbox - - io.springfox.ui - springfox-swagger-ui-rfc6570 - org.passay passay @@ -348,6 +344,17 @@ ${project.basedir}/src/main/resources + true + + thingsboard.yml + + + + ${project.basedir}/src/main/resources + false + + thingsboard.yml + 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 47674c7727..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 @@ -19,8 +19,8 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\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 \"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}", + "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/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index d0e91bedd7..9b70d91a08 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -55,8 +55,8 @@ "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 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 \"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}", + "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 \"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\"}" } }, @@ -127,8 +127,8 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\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 \"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}", + "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": { @@ -180,10 +180,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.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", + "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\":\"0px\",\"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}" } } ] 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/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json index 9e1d92d0fd..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 @@ -19,8 +19,8 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\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 \"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}", + "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\"}]}}" } }, @@ -37,8 +37,8 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "{\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 \"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}", + "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 e9ea1e0ad3..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 @@ -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\":{}}" } }, 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 f6b4d75c3a..4a2e315595 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 self.ctx.map.destroy();\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 ignoreDataUpdateOnIntervalTick: 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,iVBORw0KGgoAAAANSUhEUgAAAFAAAABdCAYAAAAyj+FzAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAH3gAAB94BHQKrYQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic3bx5uF1Flff/WVV77zPeIQMJIYQxYRRBpBGcQFEEbVQQUXB6xW5tWx9+Cm07IYIitiJog2P7qu3UCN22aDs0KIIyg0CYyUhCyHiTmzucce+qtd4/zrkhQIIogz6/9Tz1nHP22buG715D1VpVS/gLktnZjg3P2wGz3RC/N9hBCHtjMgOxGjDZv3UAkyZim4EHQO4i2j3UkxXUb9kkcrb+pcYgz3aDNvLjOah7BSZvRnwLX/8D2axILM0Dtx9ODgGGt/P4GNgtqD6A764i3+iJ44dC9CD/jaRXyqzXrHs2x/OsAGhrL9sB8W/B7ARKwz/H7TAE2btAZj89DbAW6X4HHRknnzgW7KdU5fsyeMKmp6X+J6BnDEAzhLWXHYz4c3GlG8l2HgL3frDsmWqzR5KDfpnOykkIL8D4OHPecIcI9oy09kxUaut//CKCfp7qtMuwwb8DnrOd5nOcXIfJCryAOgEpASUwh5HiBMwKEAW6YF1chIghthtqL97uSxG5C8a/TXvsBLx9VGa/8Yane6xPK4C2/kd7EuWblIZ+itU+BMx93E1OFmLchqQpTmaDA0kAlyDSwSwizkAFfN84RAfOQASLCVACDVgAESOGDZjmSDwEtYO20bVVaPMCionjiPoe2eXNy56uMT8tAJp9I2X10GfxyQTJ4GtADn3UDd6NYv5niKtgyQxcYjinkCSYi3gHikeSLhDwXjHzTNlWB4hEYnRAgoUSmIIqxAQsYEFQA/JRNHZw+lqiTn90R/VGYuNKQl5j/cTH5JD3FE917E8ZQFv23b0oJf9GNnQd8PFH1y7rkORKLJ2BTxJcAqQOSQyXGEiC+IB4wZxHXMABeIeZQ3q/MBQRhdiDNGqCaMSiYTFBKIiF64FYKBbAigKLD6PFsYjt+uhOcwHdiUMRe5fMe8uSpzL+pwSgrfje+/CyK1n1dcBeW/7wMoZll4Kfhy95SARXMsSDSxxkIInifIK4gHpH6kAlggjOOwTBJEFV8FIQDcQCGIh6ighOFdMELQKYYF1Bg0KgB2ZuxG4kFqvw+cmoDG7V/cXk7Z9iulx2efvX/1wM/iwA7bLLPIe1v4m4RSTJuWDJI/+675OUB3ClCmSC9yA1cKn1dF3mSFLDRJEsAYk9wJxSTEzQGW3TXlEQC6G7sde/0gzDZ0Zlt5Ty9Arp4GBfnBWCgxghOkIhkBsxGK4QYgs0gHZAQxPtNLH41q2GH4jhNFRfwrzK20ROis84gLbkohLp4C/IuAXso1v+UFmLlK4gLc/Bl4AMfEWQDHzZIAFJhKQMLgWzhMayjTz0kyYbrt2T2Nq3a5WFE3HWqgYzNxeU81ao5ADVpJ2ldLI6G6YN+o3zStI+CF+9n1kvWcYub6hR330mIgEtIHSAYFgOVkDREegYmoO2QfOHCJ3X4thqDirn0rUX0Kr9rex/Uv6MAWhLLiqRDPwPafdBxN79SC3yS1y9S5JVoQpSEnylB5wrgSvTAzCt0Vqzmfs/32By8cs2xZ2vXGJHjo/KnlVz1aEsLZsXKVQkpIlXTHo6T8wFjU5iTELULFpEQmN8Gsub87lq2gy/5pUM7ftb9jtjgNJO07DQQHPBerMeYqsnztaGmBvabhNbVYivemRwfJWitADktbL7OztPO4C25KISvnolWWMN8OZHasj+L5Luih9QfAWSmvVEtwquAi4DyWay6aYHuO/8A1phYOkd9sbVTbdgVlYqJVk5wycpWZaRJY7E+2i44L2Y0Zv8CkgMCMQ0xOCKGAndnCJG8m5ueacba7pkw/Pksp2rvrkH+3/kXqY9fx+cbiA0HbEDdIzYBmvRE+12l7y1GRffsRUj/JBubR6xdbQsOK37tAFol13mOXjzzymNrQL+/pGnS1/FZXvg64IfAKkqSTnBVRRXEaQ8g7HFK7jnU/NHde7tC5N3RpKB4WqpKuVq2cpZSpJlkvrMSqVEvE8ty9JClSJJJIr44BwWY0xjNC9iWZ4HH2OUdrsjzpm1Wi3X6RTW7XZpdNviwsToQfodP92tPpj9z1nG8Pxd0M4mYtugEygaGdpWQgO05aCxFI3/+Mhg5WsUQ3vx0Npj5GVnh6cHwCVfv4x0ZAViH9py0WcXIpUDcPVIOiBIFZIauKrgKg6z2Sw8c0m72XK3uA+MuMq06dVqzSqlTGq1uqVp6iuVsqRpGtIsM++ceO8NEQQi0AGmuKAElAEfYxTAQowUeZBu0U3zTkc7nVzzvGOtVpdGqyHa3rTpUPvSjpVyreDgc/dGZB3aVrRlFE3QlmBtoxhXtPUQlr/nEVTceRQzd5c9/+GUpwygLf7Ke0jHykjrS1suavpV0vqeuDqkdUEGlKTsSQYUqcxmcvky7v38c+6yt/xqvHTwvEp90OqVTKrVitVqNcrVasiyTBLnUhEBGAXW9ssqYD2QA1MT3bQP4g7APGBOv0w3M4kxxm5RxG67nUxOTkq7ndPO29YYb9pQ55a1z3U/Opb9P3wXA7vtgbbXow1HbBk6aYSGoA2hmFgK+ggnxoEPYENR5r/3y382gHb/xQeQFm/Gr/0ImOuD9zXS6p4kg4ofBKkJyYD1gCzty9IfXlVsumuXm7NPrC6Xh6sDA1XJyhWmDQ1ampa0XM689z4FNgP3A0uBdr8vSk/veXpceFj/9y39/6ccAq7/vQLsCewHDMUYQ7fbtWaz6VqtXJutCWt3OtJubBo7rPjMXsmMA5cz/60vJ3TuJzQEm1TiREJsRsKEElqrcEWfE0UJcz4P2Q9kwfvv/ZMBtD98I6XW+QXZkj0Q9uzdLZcgA8OkdYdMF5KakgwK1AWfPof7Lr52vJk27qufVq0PDKTltOoGBqo2OFi3crmszrkB4CHgAXpc5vpgSf/7MFAD9gVe0AcHYAlwK3Af0AQm+gDrVmV2H8i5ZtZpt9s2MdGwRqNJq9WUVqsZ9m1f1BqqxAr7v++laH43sWHEhhAaRhjzMBkJExNgJ/XhWUK+YDVx9FWy/9nbnN4k27oIQK39FUqLFmLhlX1beB+U67jMIRlIGiF1kApen8PCzy0cYZ91Dw2/dadp9ZqrVWtFrVZLa7UyWZaVRGQc+D2wiR73zARKqhqccwqIquKcmzSz14rImUC135uWmZ0nIrf0fw+oqjjnhJ54d4AGPU6dJSL7VqvVoSRJOlmWSKmU+KxSssVjH6zOa/5g8453nn8bzz3tEHzpDrTrECeQKnjBJSmhdQ+izwEWkNx/Oez9ReB924Jpmxxo91+wl0rjjc4vO7d3wQKu8kP84CyyIUHqgh82/ICQVA7k3m9evybstXb98FtnD9Xrrj5Qp5RlWq/Xnfd+AFgILANSVfVAHZjVf4E1EZlhZs8VEWdm3xCRD/QBfqSjIhtU9SIR+XszUxG5T0Q2quokPV25EZjsv4wA7AI838wajUaDZrujk+NjyWSzyZzJ/3pwTnrffPb5u79B2wsJEylxUonjkTAhxPENaOcURBIADfM/6bT+H7L/6Uv/KIBmiN1z/tWS3TqESM81JMlXSQb3QIYgGwQ/CH5QoDaflT+7cXNjYGLl4Lt3qA/W3WC9rtVqxdfr9QxIVPVeYKNzzoCOqjpVLSdJUg0huCRJusC4qp5FT7wPAlYCP1XVkf5zs4DXAbsCtwN7OOfO7nNiRk+EWzHGwntvzrlSn0N3APYzszjZbE50Wu1sfGKcZqNtu05+ffO0aitlj+NeSphcTGyATkCcgDhmhInlWJziutst/E1D9v2nIx/rmH08gPd+7ijSpc/BRnpW1+RWXGUd6WCGH+5xXjJo+MFhRpcu6q6+a2jJzHO65UpdBgeqbmBgwMrlciYiqqqLnXOFqk7vc4WPMRpQTZJkB1U9FEhF5Fwzu9DMbnfO/VBEXqyqrwGmHKW5c+4XZnatqr5dRA7y3p+uqh8DVFVvE5H1QAtwIhLo6dSNzrkKsEeMMQ0h5GNjY9rpFH5iYizutemTrjTvuS0G91iANcYJE544HoljQpyMxOZcsAN7SM3+AHHPhbLvP/9ua7zc47jP7OPEhz+KdcA64MIdSJKBBxFFpPcGigmx1Tc/f9G0syZrtZofHKi5en3AyuVy0gfveufc5qIo6mZWAWaZ2Rzvfdl7/3AI4SozGzCztqqeG0L4ELCPql4QYzzezFRVH1DVB8xMY4zHq+qFwHwzOyOEcF6Msamqg865K2KMK0XE05vaTDOzUgihqqojqnqD9z5kWZYMDAwmpVJGvT4gS6af1baHbzyY0FQwD67nzBVngMNz4xYcbOXHLMRzzEy2CyB3nfdicQ/8FOvMRrsQu78A2xWJYCaY6zGwxf1Y+quwbNrp19QHB6u1Wo16vUalUk5EZGdVXaiqQ3meO+/9BhFZrqqr6OnAE83sH/ttV4HvAB3n3Plmttg5d6Zz7sNm9i0zu69fvqWqH3bOnWlmy83sAhFpiMh/0JtgO+/9e1X1TTHGATNbb2YPOefGVbUEDKvqQmBOqZS5wcE6lXpdqgODlWXTP/Iblv48RePeOOt5wrUvpZrvDt3/7WMxS9zi/+aezx2+NWSPssImeqbY4j23XJDSeszvBAJODFHBWcrIA1c1sr1zN7DHjqVKhUqlQqlUcsA8M3tQVV8MNEVkjqoGEVkLPKCqVwEvA3Iz+yDwG+BMEfmtmV0hIifHGM81e3T8Z+p3jBHgTu/9h4qiOM4591ERuTKEcIb0JCMTkSuAGWa2v3Nujpk5MxsFEhFZ7ZybWyqVHq6pOrQaJuPOcxvNve6sjy2+l+Gdd8EkIAJ9LFHWYFMLokXvN9vzQWCLE2ILgPbA53bS7qrfi3WP7l+6GvxcJPRG4Kwn5LE1l/FVu63e4fM31CsV6pWKZVlm3vshVb0S2BBjTAG890PAAar6ahE5EjjPzOohhLO89xeaWQ6caWZnAS/vA3Wlqv5eRFqPAbEmIkeIyCtCCAeKSEdEPqaqfw/MVdXTReRCEXHAe4B6COEqEbk7y7LNIQRCCEWaprO894eWsmyTxuhDCG7NzH8s77X+jBcyML2AsBIxQ0xwKgTdCcl/h9kRwAJh/fds4blz5aAzVz+aA7vtE527fi7WXz87/wCwO+ZAVIgFuODZuOqmkfrrJirV+k5ZkkiWZWRZtqOZbYgxHglUnXOJiKwMIdyRZdnVIYTXmdl6EXm7qq52zn1BRD6gqv8CnAtc65z7sarua2avF5GjeQyJCH3R/IaZLXLOvSHGeB498f+AmV2oqqu89/8nxjgmIrPSNL0qhDAjhPBiM9sdiCGEpvd+JE3TOTHGDZVyOSpUNtVe/fMZI9cNMn32LliMmBnmIs4ghnshHtHrybU7Iq9/A3DRFgDNEL1l/U6uUhzRN8wbUD+vF8PAepecoHEandburbkvv7mSpZTLFSuXywnQCSFc1xezIefcc83sOOCQoig+18fgy2Z2gZl92cyON7MvAb9wzl2vqqfHGKfW2rmqLnTOPaiqK1XVJUkyD9iD3grlPX0wNwIfU9WXmNmXzGyVmf1cRN7rnDtdVS+MMTpVfTcwA/gVsFBExsyscM4dVy6XnRmxiGqTw6/cYcbqKw9nmo6CbEREURFIDPW7IGETyAwIL9PO+oZZ7516gLOP/budTZfuIXp3FT89Q9yvcaUhpNTzKLuy4TJlcmLZWPqi+2P9wIFqtUq5XDLv/d5mdqeqntLXQaNm9gBwrIisMbN5fZ10sHPuJ8C7gbu9919X1XeZ2dH0ph8Xq+p/A8tFZJr1RObFIvICM9vVzB4Efq6ql5rZXSLyfOBvRWRXVf2EiMxxzh3vnPt2jPEdgJjZcF+kZznnvuGcK6vqwar6ehG5XUSeZ6YbBcOMxIrx20v5is2UXA0NPYesRgfqIf4Bs5l0H/yDWHUZq45eec63/jDpALTg1c7dNouox9Nafh1KgKRnrhWPakKINZrtlzSnv7ZWq5UlTb1kWVYG1hZFsczMFpnZ61X1VBFJrfeKvm1mL+nruLkhhKPN7MNmdngI4Twzu8Y59wHgWjP7sIhcaGbvV9WXqGpZVVv9UlXVl6rqaX0996GpZ/v68jwze4GZfTiE8BpgjpmdZWYvCSF818wwszSE8E4zO9HMVqnqMhFZm2VZ4n0qWVay1ow31Gg0DydaBTMH3qHiEGeoy2kuv4YY3wy3zUL1WOjLq133ritxP3w+2HSgQTrwc8p7DeBqDl/ueVxi1o7Nimza5VNFqVqlkmWSpulBqvrrT3/603bTTTed6pwrPVZ3/TWRqnZf/epX3/ye97znZzHGwVKpNEtEXhVCuL3b7dLpdGzmio9XZKBb4PIKoQXSNEKrS3dJh2LytfRiFqMWTvmDe+m3X5WYne30+kUbnFkvCC38L0U+HdZ2KO9uSMxw0Wh3xyanvXHCe79TImLee8ys1O125998881HOec46aSTfrd06dIlCxYsWHDvvfcu2n///fdutVqNPM8LYNqvf/3rI733d5xwwgnjU4MaGRnZuHHjxk2zZ8/eYfr06dMvvfTS/VV12pve9KbrZWrSvhWJCHfeeWdz8eLFr5kxY8YtL3/5y1t9cOyBBx5YMn/+/N2yLMtGR0c3XXHFFUclSTI8MDBw1THHHON/+ctf2hVXXPGyer1+/ymnnHJkURSrsiwrpWlqeZ6Lc07Gh49bOty4ZJB6UcXFnh+gvcbQfAbGlcDrwaabdcfMznYJt66Yha2+A3hLr4tuBGQIbWe0V7Yp7xlJY5mQPjfUD16YJok453DOzVTVmycnJ1/kvXdFUXROOumkF8cYJ0XkkBNPPPEgM/secBpwu4hc8etf//rI4447bvyEE07YDNwAvA04cqt535L/+Z//WTk5OZmedNJJP6PnsgJIY4xVYHfgqGOPPbZ26qmn8sY3vjE/4ogjqsB/xhhf6r1/N4D3/lxVPfG6667b0O12hy+++OJ9yuXywAknnHD9qaeeuujHP/7x604++eQlIvJKVf2ZiMzy3m/IshJh2iE1Ri/dn8I2o/lGOg8NQLeKCuDWYr04l0tX38rNuoOjU+zpdHHSW2EAKiWE0PO2FTW6K0qE8fWmyXBWqdfTNLM0TUVV91fVDVtzhqp+xjl3HHC+mVXN7FQz+5CZHaSqHwVYt27dGjN7jZmdr6r7hBC+r6qnq+q5ZrYsy7LEOSchhAtCCF/ql/PN7BwzO8HM/tBut7/Sr2uVqh5iZuc7514FnA98N8Z4ppnNz/M8AmRZNg04X0SOPeWUU64WkTkrVqy4sq8bVwP7eO/FewdpdQjxMwgTG2g9tAMxr6BqGBGTdAtO4QFH7ua7qLoXrjHvEQBtGmopph6LghZCc023M/yCr5mZgk2tCBpm1th61aCqJ5vZ9WZ2DvAZoGlm58cY/11V/y/ATjvtNBe4S1VPB74FvBG4EDhTVY9xzomqmpn9j6r+SFV/FGP8jZnd1Tcmx1er1dMAhoaGZqvqN/ov4D5V/ZCqvkNVvxljvDTLMm9mOOcuNLNPm9mNr3zlK1+oqvHKK698oZmpmY2LyIRzzkTEvEinW33ORbTXdqEbUQUjwUiINn0LTtaYi9meiWg8ACkO6GPQQaRCtBwfCyIlEEOHO6jf1bmkSJIkAENmtjaE8KrR0dHvAh/pc+FewMMxxt+IyDkicq2q/peIvK//tlm+fPnyGOMxwIV9Sz1mZpc75xYCRbPZfDO9KcjuZlYDMLMNZrZSRH6rqqsnJiYAzldVAd7br2d1COHMNE33VtW/FxFijIv6n2c6537rnFNVPRxYd9ddd80xs/NDCMc659aratk5twFI1dUXEJM2lnchRlwUMI+TKpCjZEjYT0PsJBJ1fxwHA2ByD6KG8wENAWcZBYKUM4b2ayeJK8eolqZJbma5qh5YrVbX98FLb7jhhi8fcsghpyZJsjbG+Enn3Dn9acyXVRURef+ee+65B70A0b/EGDc5514A/G0I4c0A1Wr1rna7japuvadwd9VHtkEPDAxM9EV4tYicEULY0Xv/9yJyboyRGOMX77vvPpfn+SnOObrd7qdKpdJbzGxP4JMzZsw4ft26dTNijEc65w4ErnbOdfO8GEqSxJLpB25ktFzDaUSDoNERDEQdjvsxDgQ7VFRDglgZo95TZPogJlUsRpCIRkEAqQ672ryuiQQRSVV1DzObEJH/k2XZJ/uK21988cXv74vyHqr6MTMbT9O0VSqVjqHn9OTyyy/fddWqVfckSXJ4lmU2PDxMjPFmVV3TbDZHNm3a9Gag+pOf/OSHSZL4iYmJep7n3YmJCdm8eXPsdDph06ZNOwMvufLKK1/5+9///tA8z2tFUQB0RCTz3n8QwLmesynP83enaTrovf+AmV04f/7862666aY9RWSWiLwjxniqiOyRJH5pURQastkxteowRW6g1tsVZgZiRHsIOBBjEI2VBNXe3kUAtSaQoma4QsEJFoQ0rbq0hjmX9yTKhoHRWq32yRUrVnyn2Wx+qlqt0g9R4pyT/pywBAx1u48E+WOMu91yyy27sR1Kkt7y/PLLL3/L9u6ZIhGZ3e12Z2/93LZo2bJl//2c5zxnLvAl4L4sy+7tdDoHxxg/MzAw8KlOp7PUzIaT3to+NwYMyUpYXiBqRAOHEdWDTiJTLkHFoYVsUYxCCwgQlRiFEAQtDEmHVBLnvS9UNdCLV7SA3efOnfuqkZER1qxZQ6PR2OJ6+muiJEkOFZHXmdmFIvK5I488UkZGRkoDAwOvA3YVkWY/LlOYmSVpDciGevtrQi/Or7FvPGg/YnCDc72NnvRKdEqkidDEaKK2DmMN4hLvk2az2eyISINe7GIDgPd+N4CiKNi4cSMPP/wwo6Oj9EXqr4IWLVp0D/B3RVG8Kc/z7+y9995Lgc7atWt9/5YNwKoYY8PMmnnezXGuRKBFpAk0UBqYTqDoFrxMSTDskTCJ1VCGCQgmZg4vZoZnMoa8UqkMls0siTHuY2YPbauzMUYmJiaYmJggyzJqtRq1Wu0JReyZoKIoaLVaNJtNfvCDH5RGRkZeOmfOnN/ssMMOyxcsWPBGEVl37bXXPnDyySc/L8Y4R0T2EZGFIhLE2ySiE5jMwBCi9QL+Ig7byotvWALxkXi/yRCQRMU5jFhYCXGaxDgeuk2DctL39TXpBcCfkPI8J89zNm/eTJqmU55rSqXS0w5oCIH+epZOp/MoCdiwYcPJ3//+9x/3zNe//vWFJ598MmY2ADRU1RVF4V0+TmahEYxpBBVBAog6ByJWe4ThIglqG3CyCmwepkNmKIZEwUV1DlTF8gZx3GBGGmN0fZ3x+B34j9Bm59xD9BjdqequRVEMbz2oJElI05QkSciyjCRJcM6RJAkissWCAqgqU/NIVSWEQFEU9L3M5Hk+NbnfFq1yzo1OratjjPvTC8azadOmV/bvmWNmDeecxBgTjZMSi9CIQYcR5zykeFUiINQQAZEHzeLaxDTeLUYKzEPcAWLcY6ZmipppjMFI8tEGzeXW9TtnWZYCrFfVA+jtBngUOefuEhEdHh5ePnPmTN2wYYMbHx8HWNV/BmDL4J9BUu/9LWaWzJ49e/nAwIB7+OGHa51OR60XtdsdyNevX39zrVY72Dl3dwghiTGaH1+UabGpFQszEg3OgSgizjnE9u8bkdscdqczC/ejyeL+Mm6WYQ3MopmoBefMJOk21laTxuKKmbrY83aPmlkSQrgeoF6v/wxARO4WkearXvWqHdI0PcE5d2KpVDrhqKOOmiEiLefcfc8kYluT9/7mUqk0/qIXvWh/7/1JaZqeOG3atGOPPfbYrohMAmumTZt23ZIlSxaaWaqqm83MVFVKnQdrne66ajRJXHSiEcQkGjaO0lvOkSxB7QHnE1lCHFyLWc+3H2l5xKtapqYuRpNue6ySFSsXhKCxv05tAHS73dWPEZ3k2GOPnT4yMjL3C1/4AqVSic9+9rOMjIzsfNRRRw3S24X1bJysXGlmpec973kvnT59euUTn/gE8+bN4/TTT+fee+89/MADD1zhnFvfaDRmttvtrK8eGj3VECzLVy0IjfGKRiOqOtRSU0vFaE3hRJi2nsKWJ2xuL9fa7Nc7t7HXtLNGjOwgaoWpYEoSY3emaPeAPG//CkoVEcmdc4QQ5l166aVXNxqNI5MkuQ6we++99yWHHXYYt912G7vvvjs33XQTCxYs4NZbb91XRK53zt1mZjOfcPhPkURk0+zZsx9cuXLlwcPDw6xcuZKhoSHuuusuDj30UFqt1jELFy5cVhTFXO990teteVEUxJi3he4+MXYjgDnUjAg4orWm9nJo2GGmm2bLEnnrzRP6k8MPe8QS41EwkwTFFIsaRIr25qt8vmY8uHlV770lSbIaOOKFL3zh/y5evPhKgOc+97nV733vexx++OEsXbqUiYkJ5s2bx2GHHcbvfvc7jj322MV33nnnI6HUp2nSLfL4PVJHH3307BtuuIHjjz+eK664gvHxcV7xilcwOTnJ9ddfnx1wwAE3NZtNt+uuu74GeKgoCosxGq1VY3lr9CoN7CjO1KI4ExEUxZNN4SSUXiwvu+YT/bCbbRLldoSDMTnSO1seogU1cTEXb2YyvvaOscHB31dG0zdrjFG893cDx+yyyy4bP/OZz5yqqqgqt9xyCz/72c94xzveQQiBgw46iO9973ssWLCA973vfe+cum/Kom5tXacAfeznFEBbfzrnEJFHfe87erdY8F/96lf86Ec/4rTTTuOaa66h0WhwzTXXcPDBB/O2t73tnd1u99uqOlNVfwVYu92VOe3rapMjCyei2lwfxXDOnMTgRQSTl4OB8QczVkJvcyPnvGnuJDI+ihWvAKaJ2kIzqmoiqhBUpNMZr8/ace8j1snzljrnvHMud87tF2Ospmk6bGZOVVmwYAGXXHIJeZ6zdOlS1q9fzx133MEZZ5xBrVZ7FHjbA3Lr348t2+Pex3KhiDBnzhyuuOIKGo0Gq1atYs2aNaxfv54PV+O33AAAECZJREFUfehDJEkSut1uDZihqre3Wm2X5+3unPy3x2548Kpu6sV7QROPpIL3XkYxDu8teev/Kjrjl+dctnpF71W1uFnjzvVHnIV+rSBCRDDBDDWl3GmM/LrUWTbS6XQkxqiqehuwT7fb/Y+pAe68885cfPHFpGmKc45SqcQFF1zAnDlzHgfSE5W+W2pLeTLPPBbsww47jLPOOot2u02e5+y1115cdNFFZFlGv8/7qOqt3W5Xut3cyvmDGzutkSs0UlLFMHEoiIlhbJjCR8PcIWLj1p4o90kvPegaSe/bC2wOcLsZY50CaRdYNzfJI6gbWL3LIe+euyh9+4ZSqSKDg3UnIieq6n3lcnm3vsf4cQP+YwA8lgufSISfSGwfW7z32/s93ul0NojIfFX9z0ajRbPZtP35wZyVt/zbKomTcyspVkqFSoaWUkSEGcCBmKy2uN9id9LCl8NWu7NU9UKsdHEf5YMFNgjgpLcBFpCiPbkTYbKgvXp9nnes3W6rmd0I7Nduty/dlsg9FpjtgbctDvxjL2B79W2v7anS6XQuN7MFwPV5nlur1RLXfXi9FRPtojM5B3D9bXwighNYh3Fgb65culgtfmEKty0A+qHWFZrvNG/LPEddI3WGiLkE8JiKt/TB2340viD9/X7tdtva7bZ18/xhM5swszfGGK/bnu7a3oC2J7pP9P2JxPaJ9KeZEUL4dYzxFBEZy/N89cTEpO902rpP6boDlt98adNjPsE0wSQRIxEDle4ULhp3nu/Xt696HIDy6qVd1Asql/ZMdXy7F5ssiUURvDcRr1i3NT5DWxsmKvnitZONhms1m1oUxdVmVu92u0NmNj4lftsb2JPVgU9WBTwWzG2B2O/ThjzPZ5tZmuf5NZONhmu22jpkS9fE9sho0R4fRhDvRRMxSxLDY2OIvq0nmXIJKm05bWn3cQACOHFna5hzX1+MM8ytS5yId+AEBMErsui671b3Gbjn2G6n02w0GrRarY6ZXQUc0Ol0rrZetOsJOWFbYG5Ld/4xffpk2zGzTp7nd5rZc4HfdDqdvNloWrc12dqrfs+rF1373YokvU2ECeC9+FTEsGQjPbcfxLlLXJJ/+lGYPcr0n3LPerS8D/DbHhfaWxOxsVKCeYdkHhMvRAvDS2/58Y0H1X9fGZ9o0Gw26Xa7m+htAH99nuc/2NoIbP35ZET6sRZ4ayCfTB3barvb7f48xvhK4A+NRmt0YmLSJpsNe/70m+tLbrrsRrUwLe0fB08F6Z2rtzGI7+jpPq6MoTRfTlo6sl0AATq+/U+az76h/1AVlZg6o5xg3uNSj6WefHz9kj1orrCdSsvWTUxMyOjoZtrt9lLgTjN7ewjhB8CfxYl/zpTlCUpeFMUlZnYicHur1Vo+OTnpGo0GOyVLVkvzIR1bt3yPzBNTJ5Y5LHGQelPE5T1JBHTObb7onvFYvB4HYO3kVWuIpRSTb/aXLSd6WJQ6c5nHSg4p9xqS+67+9tCe9QcPk+66NZOTEzI6Okqz1VpsZjer6luLovjZ1jrxyXLlE80Dnwy3TX0C62KMv1PVk4GbGo32srGxMRkd22x0143sOfTQYXf+5t/qpQQyJ5qmWOKQLDHxxiLM3tTXfV/TIknlnSselxXpcQACuEp+Tsx3HERp9Pz/7kWZp1tySEkg8bjUiROscsvln1v7kl2Wvrzb2LhhbGxSxsfGtNlsPqSqV5jZ60IIK1X1uq3r/1PB/BNBm6Lr8zzfqKqvUNX/nZxsPjw2ttmNj08Smps2vnju4iNv+cnn1qVi1VRESg5KhpQTXObIUXdEP/bRiPnsHZzaJ7aJ1bYuykkPt73xbbR+Zu8N2P6ibqycQrmMlZ1ZKYVyIi6VOHzzjz9/99ELlh8dWhvXjI6OsmnTJhsbG5sIIfwXsIOqHhRj/Da9I1mPom0BsD0An+iZrWg8hPDdEMLzgel5nv98fHx8cnRs1MbHN2unuWHkmL0ffMXNl3/+LtEwVErFlRJcmpiVykgpRcXcJrB9eqJbP9OL/5q8c8U2T7E/4WnN8J2df+iTtQKc3If7KyGyf7ONNbqUOoXQbFtoBaJKZc3hJ33igF89sOPVOcNzhoYGQpqWy/V6RUul8qCZHqGqS4CFMcZTVHvO2a0t67asLvC41cTUiuIxn+qc+w/ghc65nUTkd3mej7dardhqtbLJZlMzHXv41c/Z8MqbL/nMIrHG9EqCr1TEVVOjVibUM0g89wFTx14viWHHkLxz9du3h5Hf3h8A57x++i9jrLzV+ZZgzACe6+B3HqYhaIw4J+JMkbwIQyvuvHryZS9//i6tXB9YuSFMC0Xs5W2KoeVElgCY2dFmttjMfk7v8M1g//qWds1sm56XKRAfc20N8J/0gvgvBO4piuKOZrMZx8YmmZycYPPmUXYb3LT+pXtu+ptrf/iJEbHujEpKUstEaplRKxNrKaTCCoR3AB6RJTGfPekle/s5Px3bbuzhjx64bn9tx92yrPigS8b+AcgQNqP8ohuZ28zxrQ7SynHNDtIulE5wxaEn/PP6WJnDT24faJfSSlKtlsvV6oCVKxmlXgCpDOypqrur6m/NbF2Mcb6qvlh7Z+keJbZbr3+dc8E5d4OILPXe7ygiR3rvV5jZgzHGVp7ntNtt2p3cmq2m73ZbjeOfN1nz3TXx1h+fv2PJaVYtOStnSK2C1lJirUxeSlmPcQwwo7e9b+gifPlf5e1rthm+fdIAAoRvzXytSHcv51rn959aCdzcDcxsdPCtHNfuIO0ca+UaWwEGZu+16pDj3vc3Ny1Nfr1oXW1WuVz2pSyxUqki5XK2dSQuU9VhM9vFzKap6gozW2NmDVUt+qcvMxGpi8iOwK5JkkyIyEOqOm5mRVC1kOd0Ojndbod2u0On0427zypWHLlv53X3XfWD69ctv3V+NcPXMmeVBKplQqWEq5eQUsZalBfSOw2PhuqHTct3J+8e+dUfw+bJZ+345vR3kbTrkE8dR1iF8LsisGujQ2xFrNUmtrvUmwXdboHP1RUvPOmfNyYDO02//BbuGu9k8yqlMlk5w7uEUlYy55A0TcQliTkRw0xxHouxd2Cot3PT+kcbxEzEQIIG0SISo1qet6UoAt08aLvb1uGyrj/hMD0gTK7ZeP2PPj+zlGhazoiVBK1lZJUSRb2CVlMk9ayldzJ+DwBi9gG01pV3b3xS2Yz+pLwx8RvDn3RZdwzrgyhsxvhJMPZq52izLVmzwFq5SbdLaEeNndyZlOutw97wwU5anz39ZzeHO9dPMCvxaZp4j08z0iQlSz1Kz/XhxHdxRFR7ESvnPIqPUTPV6LwX63aDKLEXEy6ChVDkc4Z15G8Pyw4qJtZvvOm/v1ixTqNaTpRSySXVFF/NROslpJwY1RKWeBZjnIAw1BtP9gGKclXevfmzTxaTPzlzUfzawBnOhwSXn9dPDpZj9i1DDmp2oR0Iza5qJzhrFfhuR5NOQdENTpPKwNiBx5w6MXOXfY9YtiZcef097e7IhE2XLMlSvDjn8IngncfMgllvQ7JzIoqkGsRUC4kWpCiCodadPuw2vXi/cmXBTslRIyvvvfauX/37YNGdnFbNVDJHUi67WMmIlVSpJs5XMqRWwouzuzB5J5BhqMbkDGflivzD+JMG788CECB8tfZO8cVOzsd/4pF8pz9CGOjm1DoB1+yStgq1dk6RF851g/puTtE10iLSGNpx95EDjjrFTZu928taOXcvebizfMWabvfhTV3f6UI3qqC9o6WC0ywVyiXYeUYp7rZTWpq/c3WPWsYBm9c9ePU9v7lEx9Y/uEOaUMs8oZSQVTJXZF6tZ22dK2eESkYsJf2NU0I/LwKjhPSLEb8i+YfmD/5ULP7s7G321cphEXuv98XeiL2gf3kxwu8N269VENtdF9sFaadQ6/aSDaXdSMgLfKFoEUhiJCcpjw7OnDu+497P1xk77pqV6tNr5drAoE/THUSchLy7odOcnOg2Rpub1q3M1y26zU1sXD1E6ExPElLv0MzjspRQ8iSlhKKSkpRTJ6WUolZSygmZiNyH8VKmMs2Z3BhDtlyRf83e17r1z8HhKaW/m/jywIy65N+XJP4B9JGljsiPMTSiO3cCRTt32im01A3keeF8oap57nxhWsRA0lEwJQQlMcNCz502ldqk109BEwERJHEEcSQlB6knJN6lmdOYlpzLnMZKQpZlrltJ1ZVLpB63CiPF7PgtfTT3KYv+b0TKb5F/HN/852Lw1BMwXobX9dmZmJrL9K3Agv5fOSr/jmdGVJ2bF3Q76nxRqHZy52LUmCsuj06jqu8qgjmiEtTUmEqF0vumgDlx4h2Jd2qJgHcuJk592ROT1PlSopomzpW9xiwl896tQumAvZlH0gcs1sJd4oTAxnCenP3Udko8bTlU7SvMN/NfFW9XAJ9iKmVJL/vkDzEM0V0Ldb5bqObRuagaikBWmLMQCKAuGAFDowKO3gpASbwDBJcICeI08SSJU8kceZKSpGCl1EnqNWJuBYLD7C1bsmBCC+MTMcqrfIjvlQ+y/OkY99ObhNaQ+K8ch+OdPnG3KXwco7xVY/eqyY3iqRk6xyJZUFeEqC4oFOYExaKh9JzaPSMiGOLECw6HpKKWeMyLI/XqxVMIbq1Fmk7scIP9txphxymfiUGfD3zL/3/84ulMifzMpEH+Bmls8yaMU8y535rnNOnP8rdqeA3I1eKsbSolB4NRdFgMb0Y0wcR64mXSSzggggeCw40rTIizrqlUwF5msNOj+gArPVykhR6t8P27q1x2yHt42vcdP6OZzO1sXBjmKODDqvxeEjfN4ANP0JlRRG43GBdljN5OWDCrmWNYYAizgw2mP0EdXzJ0s1NeivDZZDNXP1U990T0rOXSty8wsyuchXMrTLjgmWhDjDNQ3a1kfEr+iY3PRBuPa/PZaGSKDKR9AZc47x6KulUuwqelbrnIqe5c+SdOFJ4+HffHaJse6WeKBKwyyVtDoQca/Eb7qbSfarHItVbovpUB3vxsggfPMoAAcjahW+KNVlhDlZVPGcDIWjFbF5U3yTNgJP7oeJ7tBqdo9F84wMOpivwjsmWS+6dSMLUvpsJ3Bz7CdpMkPpP0FwMQYPyznByUHXFy4Z9VgdrpzjE+7aN8+2nu2pOmvyiAAJvO44tmboNh5/1pT8qnPTpj+se3nRjx2aK/OIBmyKbP8FMzN6bY257MM+LkMjGtzQy89pmc4z0ZetaNyGNJBGtv4k2gczVy+5MwHHcRdVoROOkvDR78FQAIMO+LtIm8zYndr8bodqcrRkuwG73jXTudTeuP1/zM019chLemtWdzpCkvir2EZI8jBx9xCTfNOYvfbev/vwT9VXDgFM05m2sMgiinP477lNMd6F8TePBXxoHQW+6tOYsfFsrDpnyof/GiJGHmzp/mrc/2SuOP0bN7CvpJkICZ4+2rjF8G5RcmDDrHvjt7Xv3XBh78lYnwFMnZhOg5SRxF6hmhyUlyNs/o2dj/X9LKT7D/yo+y31+6H09E/w/wHJVcjfUH5AAAAABJRU5ErkJggg==\",\"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\":{}}" @@ -36,7 +36,7 @@ "resources": [], "templateHtml": "", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.getSettingsSchema = function() {\n return TbTripAnimationWidget.getSettingsSchema();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true\n };\n}", + "controllerScript": "self.onInit = function() {\n var $scope = self.ctx.$scope;\n $scope.self = self;\n}\n\nself.actionSources = function() {\n return {\n 'tooltipAction': {\n name: 'widget-action.tooltip-tag-action',\n multiple: false\n }\n }\n};\n\nself.getSettingsSchema = function() {\n return TbTripAnimationWidget.getSettingsSchema();\n}\n\nself.typeParameters = function() {\n return {\n hasDataPageLink: true,\n ignoreDataUpdateOnIntervalTick: true\n };\n}", "settingsSchema": "", "dataKeySettingsSchema": "{}", "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"latitude\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 0 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"longitude\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var gpsData = [\\n37.771210000, -122.510960000,\\n 37.771990000, -122.497070000,\\n 37.772730000, -122.480740000,\\n 37.773360000, -122.466870000,\\n 37.774270000, -122.458520000,\\n 37.771980000, -122.454110000,\\n 37.768250000, -122.453380000,\\n 37.765920000, -122.456810000,\\n 37.765930000, -122.467680000,\\n 37.765500000, -122.477180000,\\n 37.765300000, -122.481660000,\\n 37.764780000, -122.493350000,\\n 37.764120000, -122.508360000,\\n 37.766410000, -122.510260000,\\n 37.770010000, -122.510830000,\\n 37.770980000, -122.510930000\\n];\\n let value = gpsData.indexOf(prevValue); \\nreturn gpsData[(value == -1 ? 1 : (value + 2) % gpsData.length)];\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"timewindow\":{\"history\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":500}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"mapProvider\":\"OpenStreetMap.Mapnik\",\"latKeyName\":\"latitude\",\"lngKeyName\":\"longitude\",\"showLabel\":true,\"label\":\"${entityName}\",\"showTooltip\":true,\"tooltipColor\":\"#fff\",\"tooltipFontColor\":\"#000\",\"tooltipOpacity\":1,\"tooltipPattern\":\"${entityName}

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

TimeStamp: ${ts:7}\",\"polygonOpacity\":0.5,\"polygonStrokeOpacity\":1,\"polygonStrokeWeight\":1,\"pointTooltipOnRightPanel\":true,\"autocloseTooltip\":true},\"title\":\"Trip Animation\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":false,\"showLegend\":false,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false},\"displayTimewindow\":true}" @@ -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 self.ctx.map.destroy();\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 ignoreDataUpdateOnIntervalTick: 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 self.ctx.map.destroy();\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 ignoreDataUpdateOnIntervalTick: 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\",\"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\":{}}" @@ -169,4 +169,4 @@ } } ] -} +} \ No newline at end of file diff --git a/application/src/main/data/upgrade/3.3.2/schema_update.sql b/application/src/main/data/upgrade/3.3.2/schema_update.sql new file mode 100644 index 0000000000..9a8c30ab63 --- /dev/null +++ b/application/src/main/data/upgrade/3.3.2/schema_update.sql @@ -0,0 +1,70 @@ +-- +-- 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. +-- + +CREATE TABLE IF NOT EXISTS entity_alarm ( + tenant_id uuid NOT NULL, + entity_type varchar(32), + entity_id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_type varchar(255) NOT NULL, + customer_id uuid, + alarm_id uuid, + CONSTRAINT entity_alarm_pkey PRIMARY KEY (entity_id, alarm_id), + CONSTRAINT fk_entity_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_status_created_time ON alarm(tenant_id, status, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_entity_alarm_created_time ON entity_alarm(tenant_id, entity_id, created_time DESC); + +INSERT INTO entity_alarm(tenant_id, entity_type, entity_id, created_time, alarm_type, customer_id, alarm_id) +SELECT tenant_id, + CASE + WHEN originator_type = 0 THEN 'TENANT' + WHEN originator_type = 1 THEN 'CUSTOMER' + WHEN originator_type = 2 THEN 'USER' + WHEN originator_type = 3 THEN 'DASHBOARD' + WHEN originator_type = 4 THEN 'ASSET' + WHEN originator_type = 5 THEN 'DEVICE' + WHEN originator_type = 6 THEN 'ALARM' + WHEN originator_type = 7 THEN 'RULE_CHAIN' + WHEN originator_type = 8 THEN 'RULE_NODE' + WHEN originator_type = 9 THEN 'ENTITY_VIEW' + WHEN originator_type = 10 THEN 'WIDGETS_BUNDLE' + WHEN originator_type = 11 THEN 'WIDGET_TYPE' + WHEN originator_type = 12 THEN 'TENANT_PROFILE' + WHEN originator_type = 13 THEN 'DEVICE_PROFILE' + WHEN originator_type = 14 THEN 'API_USAGE_STATE' + WHEN originator_type = 15 THEN 'TB_RESOURCE' + WHEN originator_type = 16 THEN 'OTA_PACKAGE' + WHEN originator_type = 17 THEN 'EDGE' + WHEN originator_type = 18 THEN 'RPC' + else 'UNKNOWN' + END, + originator_id, + created_time, + type, + customer_id, + id +FROM alarm +ON CONFLICT DO NOTHING; + +INSERT INTO entity_alarm(tenant_id, entity_type, entity_id, created_time, alarm_type, customer_id, alarm_id) +SELECT a.tenant_id, r.from_type, r.from_id, created_time, type, customer_id, id +FROM alarm a + INNER JOIN relation r ON r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id +ON CONFLICT DO NOTHING; + +DELETE FROM relation r WHERE r.relation_type_group = 'ALARM'; \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java index 32c995ac82..996ca23e9c 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardInstallApplication.java @@ -31,6 +31,7 @@ import java.util.Arrays; "org.thingsboard.server.service.install", "org.thingsboard.server.dao", "org.thingsboard.server.common.stats", + "org.thingsboard.server.common.transport.config.ssl", "org.thingsboard.server.cache"}) public class ThingsboardInstallApplication { diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java index 4d15894387..8d39cf65ce 100644 --- a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java +++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java @@ -20,13 +20,11 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; -import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.Arrays; @SpringBootConfiguration @EnableAsync -@EnableSwagger2 @EnableScheduling @ComponentScan({"org.thingsboard.server"}) public class ThingsboardServerApplication { 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 a12887db81..41d99a682b 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 @@ -719,7 +719,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { void processCredentialsUpdate(TbActorMsg msg) { if (((DeviceCredentialsUpdateNotificationMsg) msg).getDeviceCredentials().getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { sessions.forEach((k, v) -> { - notifyTransportAboutProfileUpdate(k, v, ((DeviceCredentialsUpdateNotificationMsg) msg).getDeviceCredentials()); + notifyTransportAboutDeviceCredentialsUpdate(k, v, ((DeviceCredentialsUpdateNotificationMsg) msg).getDeviceCredentials()); }); } else { sessions.forEach((sessionId, sessionMd) -> notifyTransportAboutClosedSession(sessionId, sessionMd, "device credentials updated!")); @@ -747,7 +747,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { systemContext.getTbCoreToTransportService().process(sessionMd.getSessionInfo().getNodeId(), msg); } - void notifyTransportAboutProfileUpdate(UUID sessionId, SessionInfoMetaData sessionMd, DeviceCredentials deviceCredentials) { + void notifyTransportAboutDeviceCredentialsUpdate(UUID sessionId, SessionInfoMetaData sessionMd, DeviceCredentials deviceCredentials) { ToTransportUpdateCredentialsProto.Builder notification = ToTransportUpdateCredentialsProto.newBuilder(); notification.addCredentialsId(deviceCredentials.getCredentialsId()); notification.addCredentialsValue(deviceCredentials.getCredentialsValue()); 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 8971cf1a23..38d6f4bccb 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 @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.TbMsgProcessingStackItem; import org.thingsboard.server.common.msg.queue.ServiceQueue; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; @@ -134,6 +135,25 @@ class DefaultTbContext implements TbContext { scheduleMsgWithDelay(new RuleNodeToSelfMsg(this, msg), delayMs, nodeCtx.getSelfActor()); } + @Override + public void input(TbMsg msg, RuleChainId ruleChainId) { + msg.pushToStack(nodeCtx.getSelf().getRuleChainId(), nodeCtx.getSelf().getId()); + nodeCtx.getChainActor().tell(new RuleChainInputMsg(ruleChainId, msg)); + } + + @Override + public void output(TbMsg msg, String relationType) { + TbMsgProcessingStackItem item = msg.popFormStack(); + if (item == null) { + ack(msg); + } else { + if (nodeCtx.getSelf().isDebugMode()) { + mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType); + } + nodeCtx.getChainActor().tell(new RuleChainOutputMsg(item.getRuleChainId(), item.getRuleNodeId(), relationType, msg)); + } + } + @Override public void enqueue(TbMsg tbMsg, Runnable onSuccess, Consumer onFailure) { TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator()); @@ -557,8 +577,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/actors/ruleChain/RuleChainActor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java index c019e64e1f..48785bda75 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActor.java @@ -60,6 +60,12 @@ public class RuleChainActor extends ComponentActor { + private static final String NA_RELATION_TYPE = ""; private final TbActorRef parent; private final TbActorRef self; private final Map nodeActors; @@ -201,33 +202,58 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor apply(DocumentationContext context) { + return List.of(loginEndpointApiDescription(operationNames)); + } + + @Override + public boolean supports(@NotNull DocumentationType delimiter) { + return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter); + } + }; + } + + @Bean + @Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER) + ApiListingBuilderPlugin loginEndpointListingBuilder() { + return new ApiListingBuilderPlugin() { + @Override + public void apply(ApiListingContext apiListingContext) { + if (apiListingContext.getResourceGroup().getGroupName().equals("default")) { + ApiListing apiListing = apiListingContext.apiListingBuilder().build(); + if (apiListing.getResourcePath().equals("/api/auth/login")) { + apiListingContext.apiListingBuilder().tags(Set.of(new Tag("login-endpoint", "Login Endpoint"))); + apiListingContext.apiListingBuilder().description("Login Endpoint"); + } + } + } + + @Override + public boolean supports(@NotNull DocumentationType delimiter) { + return DocumentationType.SWAGGER_2.equals(delimiter) || DocumentationType.OAS_30.equals(delimiter); + } + }; + } + + @Bean + UiConfiguration uiConfig() { + return UiConfigurationBuilder.builder() + .deepLinking(true) + .displayOperationId(false) + .defaultModelsExpandDepth(1) + .defaultModelExpandDepth(1) + .defaultModelRendering(ModelRendering.EXAMPLE) + .displayRequestDuration(false) + .docExpansion(DocExpansion.NONE) + .filter(false) + .maxDisplayedTags(null) + .operationsSorter(OperationsSorter.ALPHA) + .showExtensions(false) + .showCommonExtensions(false) + .supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS) + .validatorUrl(null) + .persistAuthorization(true) + .syntaxHighlightActivate(true) + .syntaxHighlightTheme("agate") + .build(); + } + + private SecurityScheme httpLogin() { + return HttpLoginPasswordScheme + .X_AUTHORIZATION_BUILDER + .loginEndpoint("/api/auth/login") + .name("HTTP login form") + .description("Enter Username / Password") + .build(); } private SecurityContext securityContext() { return SecurityContext.builder() .securityReferences(defaultAuth()) - .forPaths(securityPaths()) + .operationSelector(securityPathOperationSelector()) .build(); } @@ -107,11 +222,8 @@ public class SwaggerConfiguration { return regex(apiPathRegex); } - private Predicate securityPaths() { - return and( - regex(securityPathRegex), - not(regex(nonSecurityPathRegex)) - ); + private Predicate securityPathOperationSelector() { + return new SecurityPathOperationSelector(securityPathRegex, nonSecurityPathRegex); } List defaultAuth() { @@ -120,18 +232,166 @@ public class SwaggerConfiguration { authorizationScopes[1] = new AuthorizationScope(Authority.TENANT_ADMIN.name(), "Tenant administrator"); authorizationScopes[2] = new AuthorizationScope(Authority.CUSTOMER_USER.name(), "Customer"); return newArrayList( - new SecurityReference("X-Authorization", authorizationScopes)); + new SecurityReference("HTTP login form", authorizationScopes)); } private ApiInfo apiInfo() { + String apiVersion = version; + if (StringUtils.isEmpty(apiVersion)) { + apiVersion = appVersion; + } return new ApiInfoBuilder() .title(title) .description(description) .contact(new Contact(contactName, contactUrl, contactEmail)) .license(licenseTitle) .licenseUrl(licenseUrl) - .version(version) + .version(apiVersion) .build(); } + private ApiDescription loginEndpointApiDescription(final CachingOperationNameGenerator operationNames) { + return new ApiDescription(null, "/api/auth/login", "Login method to get user JWT token data", "Login endpoint", Collections.singletonList( + new OperationBuilder(operationNames) + .summary("Login method to get user JWT token data") + .tags(Set.of("login-endpoint")) + .authorizations(new ArrayList<>()) + .position(0) + .codegenMethodNameStem("loginPost") + .method(HttpMethod.POST) + .notes("Login method used to authenticate user and get JWT token data.\n\nValue of the response **token** " + + "field can be used as **X-Authorization** header value:\n\n`X-Authorization: Bearer $JWT_TOKEN_VALUE`.") + .requestParameters( + List.of( + new RequestParameterBuilder() + .in(ParameterType.BODY) + .required(true) + .description("Login request") + .content(c -> + c.requestBody(true) + .representation(MediaType.APPLICATION_JSON) + .apply(classRepresentation(LoginRequest.class, false)) + ) + .build() + ) + ) + .responses(loginResponses()) + .build() + ), false); + } + + private Collection loginResponses() { + List responses = new ArrayList<>(); + responses.add( + new ResponseBuilder() + .code("200") + .description("OK") + .representation(MediaType.APPLICATION_JSON) + .apply(classRepresentation(LoginResponse.class, true)). + build() + ); + responses.addAll(loginErrorResponses()); + return responses; + } + + /** Helper methods **/ + + private List defaultErrorResponses(boolean isPost) { + return List.of( + errorResponse("400", "Bad Request", + ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)), + errorResponse("401", "Unauthorized", + ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), + errorResponse("403", "Forbidden", + ThingsboardErrorResponse.of("You don't have permission to perform this operation!", + ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN)), + errorResponse("404", "Not Found", + ThingsboardErrorResponse.of("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND)), + errorResponse("429", "Too Many Requests", + ThingsboardErrorResponse.of("Too many requests for current tenant!", + ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS)) + ); + } + + private List loginErrorResponses() { + return List.of( + errorResponse("401", "Unauthorized", + List.of( + errorExample("bad-credentials", "Bad credentials", + ThingsboardErrorResponse.of("Invalid username or password", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), + errorExample("token-expired", "JWT token expired", + ThingsboardErrorResponse.of("Token has expired", ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED)), + errorExample("account-disabled", "Disabled account", + ThingsboardErrorResponse.of("User account is not active", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), + errorExample("account-locked", "Locked account", + ThingsboardErrorResponse.of("User account is locked due to security policy", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)), + errorExample("authentication-failed", "General authentication error", + ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED)) + ) + ), + errorResponse("401 ", "Unauthorized (**Expired credentials**)", + List.of( + errorExample("credentials-expired", "Expired credentials", + ThingsboardCredentialsExpiredResponse.of("User password expired!", RandomStringUtils.randomAlphanumeric(30))) + ), ThingsboardCredentialsExpiredResponse.class + ) + ); + } + + private Response errorResponse(String code, String description, ThingsboardErrorResponse example) { + return errorResponse(code, description, List.of(errorExample("error-code-" + code, description, example))); + } + + private Response errorResponse(String code, String description, List examples) { + return errorResponse(code, description, examples, ThingsboardErrorResponse.class); + } + + private Response errorResponse(String code, String description, List examples, + Class errorResponseClass) { + return new ResponseBuilder() + .code(code) + .description(description) + .examples(examples) + .representation(MediaType.APPLICATION_JSON) + .apply(classRepresentation(errorResponseClass, true)) + .build(); + } + + private Example errorExample(String id, String summary, ThingsboardErrorResponse example) { + return new ExampleBuilder() + .mediaType(MediaType.APPLICATION_JSON_VALUE) + .summary(summary) + .id(id) + .value(example).build(); + } + + private Consumer classRepresentation(Class clazz, boolean isResponse) { + return r -> r.model( + m -> + m.referenceModel(ref -> + ref.key(k -> + k.qualifiedModelName(q -> + q.namespace(clazz.getPackageName()) + .name(clazz.getSimpleName())).isResponse(isResponse))) + ); + } + + private static class SecurityPathOperationSelector implements Predicate { + + private final Predicate securityPathSelector; + + SecurityPathOperationSelector(String securityPathRegex, String nonSecurityPathRegex) { + this.securityPathSelector = regex(securityPathRegex).and( + not( + regex(nonSecurityPathRegex) + )); + } + + @Override + public boolean test(OperationContext operationContext) { + return this.securityPathSelector.test(operationContext.requestMappingPattern()); + } + } + + } diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 2718b455b9..1a5b2f86fd 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -64,6 +64,7 @@ import java.util.List; public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapter { public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; + public static final String JWT_TOKEN_HEADER_PARAM_V2 = "Authorization"; public static final String JWT_TOKEN_QUERY_PARAM = "token"; public static final String WEBJARS_ENTRY_POINT = "/webjars/**"; diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java index 500d1bd06e..c20ec43fb7 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java +++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java @@ -17,13 +17,24 @@ package org.thingsboard.server.config; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.thingsboard.server.utils.MiscUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; @Controller public class WebConfig { - @RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**"}) + @RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)(?!swagger-ui$)[^\\.]*}/**"}) public String redirect() { return "forward:/index.html"; } + @RequestMapping("/swagger-ui.html") + public void redirectSwagger(HttpServletRequest request, HttpServletResponse response) throws IOException { + String baseUrl = MiscUtils.constructBaseUrl(request); + response.sendRedirect(baseUrl + "/swagger-ui/"); + } + } 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 e896243f04..c339d8d49e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -41,12 +41,13 @@ 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") public class AdminController extends BaseController { - public static final String SYS_ADMIN_AUTHORITY_ONLY = " Available for users with System Administrator ('SYS_ADMIN') authority only."; @Autowired private MailService mailService; @@ -63,7 +64,7 @@ public class AdminController extends BaseController { 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." + SYS_ADMIN_AUTHORITY_ONLY) + 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 @@ -72,7 +73,7 @@ public class AdminController extends BaseController { @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)); + AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key), "No Administration settings found for key: " + key); if (adminSettings.getKey().equals("mail")) { ((ObjectNode) adminSettings.getJsonValue()).remove("password"); } @@ -86,7 +87,7 @@ 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." + SYS_ADMIN_AUTHORITY_ONLY) + "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings", method = RequestMethod.POST) @ResponseBody @@ -109,7 +110,7 @@ public class AdminController extends BaseController { } @ApiOperation(value = "Get the Security Settings object", - notes = "Get the Security Settings object that contains password policy, etc." + SYS_ADMIN_AUTHORITY_ONLY) + 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 @@ -123,7 +124,7 @@ public class AdminController extends BaseController { } @ApiOperation(value = "Update Security Settings (saveSecuritySettings)", - notes = "Updates the Security Settings object that contains password policy, etc." + SYS_ADMIN_AUTHORITY_ONLY) + 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 @@ -141,7 +142,7 @@ 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. " + SYS_ADMIN_AUTHORITY_ONLY) + "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( @@ -165,7 +166,7 @@ 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. " - + SYS_ADMIN_AUTHORITY_ONLY) + + SYSTEM_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST) public void sendTestSms( @@ -181,7 +182,7 @@ public class AdminController extends BaseController { @ApiOperation(value = "Check for new Platform Releases (checkUpdates)", notes = "Check notifications about new platform releases. " - + SYS_ADMIN_AUTHORITY_ONLY) + + 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..9920e232de 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,47 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @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 +102,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 +119,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 " + UUID_WIKI_LINK + + "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 +153,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 +173,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 +202,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 +226,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 +276,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 +326,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 94c741c774..b03e446c7b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -64,6 +64,27 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +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.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE; @@ -80,7 +101,8 @@ public class AssetController extends BaseController { @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.", produces = MediaType.APPLICATION_JSON_VALUE) + "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 @@ -98,7 +120,8 @@ 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, produces = MediaType.APPLICATION_JSON_VALUE) + "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 @@ -114,10 +137,10 @@ 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) " + + notes = "Creates or Updates the Asset. When creating asset, platform generates Asset Id as " + UUID_WIKI_LINK + "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.", produces = MediaType.APPLICATION_JSON_VALUE) + "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 @@ -133,7 +156,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) { @@ -143,9 +166,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) { @@ -158,7 +181,7 @@ 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.") + 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) @@ -187,7 +210,7 @@ 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.", produces = MediaType.APPLICATION_JSON_VALUE) + 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 @@ -223,7 +246,7 @@ 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.", produces = MediaType.APPLICATION_JSON_VALUE) + 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 @@ -261,7 +284,7 @@ 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.", produces = MediaType.APPLICATION_JSON_VALUE) + "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 @@ -290,7 +313,7 @@ public class AssetController extends BaseController { @ApiOperation(value = "Get Tenant Assets (getTenantAssets)", notes = "Returns a page of assets owned by tenant. " + - PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE) + 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 @@ -322,7 +345,7 @@ 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, produces = MediaType.APPLICATION_JSON_VALUE) + 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 @@ -354,7 +377,7 @@ 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.", produces = MediaType.APPLICATION_JSON_VALUE) + "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 @@ -518,8 +541,11 @@ 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. " + - "The Edge is a software product for edge computing. " + - "It allows bringing data analysis and management to the edge, while seamlessly synchronizing with the platform server (cloud). ", produces = MediaType.APPLICATION_JSON_VALUE) + 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 @@ -554,7 +580,12 @@ public class AssetController extends BaseController { } @ApiOperation(value = "Unassign asset from edge (unassignAssetFromEdge)", - notes = "Clears assignment of the asset to the edge", produces = MediaType.APPLICATION_JSON_VALUE) + 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 @@ -648,8 +679,9 @@ public class AssetController extends BaseController { @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 ca982dbd3f..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,8 +15,6 @@ */ 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; @@ -50,13 +48,13 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; 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.auth.jwt.RefreshTokenRepository; -import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; -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; 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 f1e8f06d5e..a6ebaa4c99 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -147,6 +147,8 @@ 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 @@ -155,50 +157,6 @@ public abstract class BaseController { /*Swagger UI description*/ - public static final String CUSTOMER_ID = "customerId"; - public static final String TENANT_ID = "tenantId"; - - public 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. "; - public static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - public static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - public static final String DEVICE_PROFILE_ID_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - public static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - public static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - public static final String CUSTOMER_ID_PARAM_DESCRIPTION = "A string value representing the customer id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - public static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; - - - protected final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page"; - protected final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0"; - protected final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile"; - protected final String ASSET_TYPE_DESCRIPTION = "Asset type"; - - protected final String ASSET_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the asset name."; - protected final String DASHBOARD_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the dashboard title."; - protected final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device name."; - protected final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the customer title."; - protected final String SORT_PROPERTY_DESCRIPTION = "Property of entity to sort by"; - protected final String DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title"; - protected final String CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, city"; - protected final String DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deviceProfileName, label, customerTitle"; - protected final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle"; - protected final String SORT_ORDER_DESCRIPTION = "Sort order. ASC (ASCENDING) or DESC (DESCENDING)"; - protected final String SORT_ORDER_ALLOWABLE_VALUES = "ASC, DESC"; - protected 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 final String ASSET_INFO_DESCRIPTION = "Asset Info is an extension of the default Asset object that contains information about the assigned customer name. "; - - - protected final String DEVICE_NAME_DESCRIPTION = "A string value representing the Device name."; - protected final String ASSET_NAME_DESCRIPTION = "A string value representing the Asset name."; - - 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; - private static final ObjectMapper json = new ObjectMapper(); @Autowired @@ -356,22 +314,30 @@ public abstract class BaseController { } else if (exception instanceof MessagingException) { return new ThingsboardException("Unable to send mail: " + exception.getMessage(), ThingsboardErrorCode.GENERAL); } else { - return new ThingsboardException(exception.getMessage(), ThingsboardErrorCode.GENERAL); + return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.GENERAL); } } T checkNotNull(T reference) throws ThingsboardException { + return checkNotNull(reference, "Requested item wasn't found!"); + } + + T checkNotNull(T reference, String notFoundMessage) throws ThingsboardException { if (reference == null) { - throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); + throw new ThingsboardException(notFoundMessage, ThingsboardErrorCode.ITEM_NOT_FOUND); } return reference; } T checkNotNull(Optional reference) throws ThingsboardException { + return checkNotNull(reference, "Requested item wasn't found!"); + } + + T checkNotNull(Optional reference, String notFoundMessage) throws ThingsboardException { if (reference.isPresent()) { return reference.get(); } else { - throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); + throw new ThingsboardException(notFoundMessage, ThingsboardErrorCode.ITEM_NOT_FOUND); } } @@ -431,7 +397,7 @@ public abstract class BaseController { try { validateId(tenantId, INCORRECT_TENANT_ID + tenantId); Tenant tenant = tenantService.findTenantById(tenantId); - checkNotNull(tenant); + checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant); return tenant; } catch (Exception e) { @@ -443,7 +409,7 @@ public abstract class BaseController { try { validateId(tenantId, INCORRECT_TENANT_ID + tenantId); TenantInfo tenant = tenantService.findTenantInfoById(tenantId); - checkNotNull(tenant); + checkNotNull(tenant, "Tenant with id [" + tenantId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant); return tenant; } catch (Exception e) { @@ -455,7 +421,7 @@ public abstract class BaseController { try { validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId); TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(getTenantId(), tenantProfileId); - checkNotNull(tenantProfile); + checkNotNull(tenantProfile, "Tenant profile with id [" + tenantProfileId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, operation); return tenantProfile; } catch (Exception e) { @@ -471,7 +437,7 @@ public abstract class BaseController { try { validateId(customerId, "Incorrect customerId " + customerId); Customer customer = customerService.findCustomerById(getTenantId(), customerId); - checkNotNull(customer); + checkNotNull(customer, "Customer with id [" + customerId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.CUSTOMER, operation, customerId, customer); return customer; } catch (Exception e) { @@ -483,7 +449,7 @@ public abstract class BaseController { try { validateId(userId, "Incorrect userId " + userId); User user = userService.findUserById(getCurrentUser().getTenantId(), userId); - checkNotNull(user); + checkNotNull(user, "User with id [" + userId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.USER, operation, userId, user); return user; } catch (Exception e) { @@ -502,7 +468,9 @@ public abstract class BaseController { protected void checkEntityId(EntityId entityId, Operation operation) throws ThingsboardException { try { - checkNotNull(entityId); + if (entityId == null) { + throw new ThingsboardException("Parameter entityId can't be empty!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } validateId(entityId.getId(), "Incorrect entityId " + entityId); switch (entityId.getEntityType()) { case ALARM: @@ -568,7 +536,7 @@ public abstract class BaseController { try { validateId(deviceId, "Incorrect deviceId " + deviceId); Device device = deviceService.findDeviceById(getCurrentUser().getTenantId(), deviceId); - checkNotNull(device); + checkNotNull(device, "Device with id [" + deviceId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device); return device; } catch (Exception e) { @@ -580,7 +548,7 @@ public abstract class BaseController { try { validateId(deviceId, "Incorrect deviceId " + deviceId); DeviceInfo device = deviceService.findDeviceInfoById(getCurrentUser().getTenantId(), deviceId); - checkNotNull(device); + checkNotNull(device, "Device with id [" + deviceId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE, operation, deviceId, device); return device; } catch (Exception e) { @@ -592,7 +560,7 @@ public abstract class BaseController { try { validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId); DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId); - checkNotNull(deviceProfile); + checkNotNull(deviceProfile, "Device profile with id [" + deviceProfileId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile); return deviceProfile; } catch (Exception e) { @@ -604,7 +572,7 @@ public abstract class BaseController { try { validateId(entityViewId, "Incorrect entityViewId " + entityViewId); EntityView entityView = entityViewService.findEntityViewById(getCurrentUser().getTenantId(), entityViewId); - checkNotNull(entityView); + checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView); return entityView; } catch (Exception e) { @@ -616,7 +584,7 @@ public abstract class BaseController { try { validateId(entityViewId, "Incorrect entityViewId " + entityViewId); EntityViewInfo entityView = entityViewService.findEntityViewInfoById(getCurrentUser().getTenantId(), entityViewId); - checkNotNull(entityView); + checkNotNull(entityView, "Entity view with id [" + entityViewId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, operation, entityViewId, entityView); return entityView; } catch (Exception e) { @@ -628,7 +596,7 @@ public abstract class BaseController { try { validateId(assetId, "Incorrect assetId " + assetId); Asset asset = assetService.findAssetById(getCurrentUser().getTenantId(), assetId); - checkNotNull(asset); + checkNotNull(asset, "Asset with id [" + assetId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset); return asset; } catch (Exception e) { @@ -640,7 +608,7 @@ public abstract class BaseController { try { validateId(assetId, "Incorrect assetId " + assetId); AssetInfo asset = assetService.findAssetInfoById(getCurrentUser().getTenantId(), assetId); - checkNotNull(asset); + checkNotNull(asset, "Asset with id [" + assetId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ASSET, operation, assetId, asset); return asset; } catch (Exception e) { @@ -652,7 +620,7 @@ public abstract class BaseController { try { validateId(alarmId, "Incorrect alarmId " + alarmId); Alarm alarm = alarmService.findAlarmByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); - checkNotNull(alarm); + checkNotNull(alarm, "Alarm with id [" + alarmId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarm); return alarm; } catch (Exception e) { @@ -664,7 +632,7 @@ public abstract class BaseController { try { validateId(alarmId, "Incorrect alarmId " + alarmId); AlarmInfo alarmInfo = alarmService.findAlarmInfoByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); - checkNotNull(alarmInfo); + checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo); return alarmInfo; } catch (Exception e) { @@ -676,7 +644,7 @@ public abstract class BaseController { try { validateId(widgetsBundleId, "Incorrect widgetsBundleId " + widgetsBundleId); WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleById(getCurrentUser().getTenantId(), widgetsBundleId); - checkNotNull(widgetsBundle); + checkNotNull(widgetsBundle, "Widgets bundle with id [" + widgetsBundleId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.WIDGETS_BUNDLE, operation, widgetsBundleId, widgetsBundle); return widgetsBundle; } catch (Exception e) { @@ -688,7 +656,7 @@ public abstract class BaseController { try { validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(getCurrentUser().getTenantId(), widgetTypeId); - checkNotNull(widgetTypeDetails); + checkNotNull(widgetTypeDetails, "Widget type with id [" + widgetTypeId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, operation, widgetTypeId, widgetTypeDetails); return widgetTypeDetails; } catch (Exception e) { @@ -700,7 +668,7 @@ public abstract class BaseController { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); Dashboard dashboard = dashboardService.findDashboardById(getCurrentUser().getTenantId(), dashboardId); - checkNotNull(dashboard); + checkNotNull(dashboard, "Dashboard with id [" + dashboardId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboard); return dashboard; } catch (Exception e) { @@ -712,7 +680,7 @@ public abstract class BaseController { try { validateId(edgeId, "Incorrect edgeId " + edgeId); Edge edge = edgeService.findEdgeById(getTenantId(), edgeId); - checkNotNull(edge); + checkNotNull(edge, "Edge with id [" + edgeId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); return edge; } catch (Exception e) { @@ -724,7 +692,7 @@ public abstract class BaseController { try { validateId(edgeId, "Incorrect edgeId " + edgeId); EdgeInfo edge = edgeService.findEdgeInfoById(getCurrentUser().getTenantId(), edgeId); - checkNotNull(edge); + checkNotNull(edge, "Edge with id [" + edgeId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.EDGE, operation, edgeId, edge); return edge; } catch (Exception e) { @@ -736,7 +704,7 @@ public abstract class BaseController { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(getCurrentUser().getTenantId(), dashboardId); - checkNotNull(dashboardInfo); + checkNotNull(dashboardInfo, "Dashboard with id [" + dashboardId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.DASHBOARD, operation, dashboardId, dashboardInfo); return dashboardInfo; } catch (Exception e) { @@ -774,7 +742,7 @@ public abstract class BaseController { protected RuleChain checkRuleChain(RuleChainId ruleChainId, Operation operation) throws ThingsboardException { validateId(ruleChainId, "Incorrect ruleChainId " + ruleChainId); RuleChain ruleChain = ruleChainService.findRuleChainById(getCurrentUser().getTenantId(), ruleChainId); - checkNotNull(ruleChain); + checkNotNull(ruleChain, "Rule chain with id [" + ruleChainId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.RULE_CHAIN, operation, ruleChainId, ruleChain); return ruleChain; } @@ -782,7 +750,7 @@ public abstract class BaseController { protected RuleNode checkRuleNode(RuleNodeId ruleNodeId, Operation operation) throws ThingsboardException { validateId(ruleNodeId, "Incorrect ruleNodeId " + ruleNodeId); RuleNode ruleNode = ruleChainService.findRuleNodeById(getTenantId(), ruleNodeId); - checkNotNull(ruleNode); + checkNotNull(ruleNode, "Rule node with id [" + ruleNodeId + "] is not found"); checkRuleChain(ruleNode.getRuleChainId(), operation); return ruleNode; } @@ -791,7 +759,7 @@ public abstract class BaseController { try { validateId(resourceId, "Incorrect resourceId " + resourceId); TbResource resource = resourceService.findResourceById(getCurrentUser().getTenantId(), resourceId); - checkNotNull(resource); + checkNotNull(resource, "Resource with id [" + resourceId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resource); return resource; } catch (Exception e) { @@ -803,7 +771,7 @@ public abstract class BaseController { try { validateId(resourceId, "Incorrect resourceId " + resourceId); TbResourceInfo resourceInfo = resourceService.findResourceInfoById(getCurrentUser().getTenantId(), resourceId); - checkNotNull(resourceInfo); + checkNotNull(resourceInfo, "Resource with id [" + resourceId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.TB_RESOURCE, operation, resourceId, resourceInfo); return resourceInfo; } catch (Exception e) { @@ -815,7 +783,7 @@ public abstract class BaseController { try { validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId); OtaPackage otaPackage = otaPackageService.findOtaPackageById(getCurrentUser().getTenantId(), otaPackageId); - checkNotNull(otaPackage); + checkNotNull(otaPackage, "OTA package with id [" + otaPackageId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackage); return otaPackage; } catch (Exception e) { @@ -827,7 +795,7 @@ public abstract class BaseController { try { validateId(otaPackageId, "Incorrect otaPackageId " + otaPackageId); OtaPackageInfo otaPackageIn = otaPackageService.findOtaPackageInfoById(getCurrentUser().getTenantId(), otaPackageId); - checkNotNull(otaPackageIn); + checkNotNull(otaPackageIn, "OTA package with id [" + otaPackageId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.OTA_PACKAGE, operation, otaPackageId, otaPackageIn); return otaPackageIn; } catch (Exception e) { @@ -839,7 +807,7 @@ public abstract class BaseController { try { validateId(rpcId, "Incorrect rpcId " + rpcId); Rpc rpc = rpcService.findById(getCurrentUser().getTenantId(), rpcId); - checkNotNull(rpc); + checkNotNull(rpc, "RPC with id [" + rpcId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.RPC, operation, rpcId, rpc); return rpc; } catch (Exception e) { 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 a2bb902dad..d365d06ca5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -35,6 +35,8 @@ 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") @@ -47,7 +49,7 @@ public class ComponentDescriptorController extends BaseController { @ApiOperation(value = "Get Component Descriptor (getComponentDescriptorByClazz)", notes = "Gets the Component Descriptor object using class name from the path parameters. " + - COMPONENT_DESCRIPTOR_DEFINITION) + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/component/{componentDescriptorClazz:.+}", method = RequestMethod.GET) @ResponseBody @@ -64,7 +66,7 @@ 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) + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET) @ResponseBody @@ -83,7 +85,7 @@ 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) + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET) @ResponseBody 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..c2f965beba --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -0,0 +1,1527 @@ +/** + * 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 String UUID_WIKI_LINK = "[time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address)). "; + 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 EDGE_ID = "edgeId"; + 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 RULE_NODE_ID_PARAM_DESCRIPTION = "A string value representing the rule node 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 335140949a..f8f3a605a8 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CustomerController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CustomerController.java @@ -47,6 +47,21 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -57,7 +72,8 @@ public class CustomerController extends BaseController { "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) + 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 @@ -79,7 +95,8 @@ 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) + 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 @@ -101,7 +118,8 @@ public class CustomerController extends BaseController { } @ApiOperation(value = "Get Customer Title (getCustomerTitleById)", - notes = "Get the title of the customer. " + CUSTOMER_SECURITY_CHECK) + 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 @@ -119,10 +137,10 @@ 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) " + + notes = "Creates or Updates the Customer. When creating customer, platform generates Customer Id as " + UUID_WIKI_LINK + "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.") + "Referencing non-existing Customer Id will cause 'Not Found' error." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer", method = RequestMethod.POST) @ResponseBody @@ -153,7 +171,9 @@ 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.") + 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) @@ -187,7 +207,7 @@ public class CustomerController extends BaseController { @ApiOperation(value = "Get Tenant Customers (getCustomers)", notes = "Returns a page of customers owned by tenant. " + - PAGE_DATA_PARAMETERS) + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody @@ -212,7 +232,7 @@ public class CustomerController extends BaseController { } @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)", - notes = "Get the Customer using Customer Title. Available for users with 'Tenant Administrator' authority only.") + notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET) @ResponseBody @@ -221,7 +241,7 @@ public class CustomerController extends BaseController { @RequestParam String customerTitle) throws ThingsboardException { try { TenantId tenantId = getCurrentUser().getTenantId(); - return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle)); + return checkNotNull(customerService.findCustomerByTenantIdAndTitle(tenantId, customerTitle), "Customer with title [" + customerTitle + "] is not found"); } catch (Exception e) { throw handleException(e); } 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 3f87a321d8..1d3265053d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -19,6 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.Example; +import io.swagger.annotations.ExampleProperty; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -31,6 +34,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; @@ -50,8 +54,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; import org.thingsboard.server.service.security.permission.Operation; @@ -62,6 +64,30 @@ 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_ID; +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.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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @RestController @TbCoreComponent @RequestMapping("/api") @@ -75,7 +101,7 @@ public class DashboardController extends BaseController { 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)", @@ -84,6 +110,7 @@ public class DashboardController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET) @ResponseBody + @ApiResponse(code = 200, message = "OK", examples = @Example(value = @ExampleProperty(value = "1636023857137", mediaType = "application/json"))) public long getServerTime() throws ThingsboardException { return System.currentTimeMillis(); } @@ -96,6 +123,7 @@ public class DashboardController extends BaseController { @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/dashboard/maxDatapointsLimit", method = RequestMethod.GET) @ResponseBody + @ApiResponse(code = 200, message = "OK", examples = @Example(value = @ExampleProperty(value = "5000", mediaType = "application/json"))) public long getMaxDatapointsLimit() throws ThingsboardException { return maxDatapointsLimit; } @@ -120,7 +148,7 @@ public class DashboardController extends BaseController { } @ApiOperation(value = "Get Dashboard (getDashboardById)", - notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION, + 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')") @@ -139,11 +167,11 @@ 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)." + + notes = "Create or update the Dashboard. When creating dashboard, platform generates Dashboard Id as " + UUID_WIKI_LINK + "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. " + - "Only users with 'TENANT_ADMIN') authority may create the dashboards.", + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -177,7 +205,7 @@ public class DashboardController extends BaseController { } @ApiOperation(value = "Delete the Dashboard (deleteDashboard)", - notes = "Delete the Dashboard. Only users with 'TENANT_ADMIN') authority may delete the dashboards.") + notes = "Delete the Dashboard." + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.DELETE) @ResponseStatus(value = HttpStatus.OK) @@ -211,7 +239,7 @@ 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. Only users with 'TENANT_ADMIN') authority may assign the dashboards to customers.", + "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) @@ -251,7 +279,7 @@ 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. Only users with 'TENANT_ADMIN') authority may unassign the dashboards from customers.", + "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) @@ -290,7 +318,7 @@ 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. Only users with 'TENANT_ADMIN') authority may assign the dashboards to customers.", + "Returns the Dashboard object. " + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @@ -365,7 +393,7 @@ 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. Only users with 'TENANT_ADMIN') authority may assign the dashboards to customers.", + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -417,7 +445,7 @@ 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. Only users with 'TENANT_ADMIN') authority may assign the dashboards to customers.", + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @@ -473,7 +501,7 @@ public class DashboardController extends BaseController { "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. Only users with 'TENANT_ADMIN') authority may assign the dashboards to customers.", + "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) @@ -505,7 +533,7 @@ 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. Only users with 'TENANT_ADMIN') authority may assign the dashboards to customers.", + "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) @@ -538,7 +566,7 @@ 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 + - "Only users with 'SYS_ADMIN' authority may use this method.", + SYSTEM_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @@ -567,8 +595,8 @@ 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 + - "Only users with 'TENANT_ADMIN' authority may use this method.", + 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) @@ -600,8 +628,8 @@ 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 + - "Only users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority may use this method.", + 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) @@ -641,8 +669,7 @@ public class DashboardController extends BaseController { 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 + " " + - "Only users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority should use this method.", + + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET) @@ -679,7 +706,7 @@ public class DashboardController extends BaseController { 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. " + - "Only users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority should use this method.", + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("isAuthenticated()") @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) @@ -714,7 +741,7 @@ 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. " + - "Only users with 'TENANT_ADMIN' authority may use this method.", + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) @@ -740,7 +767,7 @@ public class DashboardController extends BaseController { @ApiOperation(value = "Update Tenant Home Dashboard Info (getTenantHomeDashboardInfo)", notes = "Update the home dashboard assignment for the current tenant. " + - "Only users with 'TENANT_ADMIN' authority may use this method.", + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST) @@ -805,6 +832,14 @@ public class DashboardController extends BaseController { 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 @@ -838,6 +873,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 @@ -870,24 +913,32 @@ public class DashboardController extends BaseController { } } + @ApiOperation(value = "Get Edge Dashboards (getEdgeDashboards)", + notes = "Returns a page of dashboard info objects assigned to the specified edge. " + + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, + produces = MediaType.APPLICATION_JSON_VALUE) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/edge/{edgeId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getEdgeDashboards( - @PathVariable("edgeId") String strEdgeId, + @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 = 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, - @RequestParam(required = false) String sortOrder, - @RequestParam(required = false) Long startTime, - @RequestParam(required = false) Long endTime) throws ThingsboardException { + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES) + @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("edgeId", strEdgeId); try { TenantId tenantId = getCurrentUser().getTenantId(); EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); checkEdgeId(edgeId, Operation.READ); - TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); PageData nonFilteredResult = dashboardService.findDashboardsByTenantIdAndEdgeId(tenantId, edgeId, pageLink); List filteredDashboards = nonFilteredResult.getData().stream().filter(dashboardInfo -> { try { 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 f0991ff901..6ea6bdd880 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -24,6 +24,7 @@ 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; @@ -45,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; @@ -85,6 +87,32 @@ 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.ControllerConstants.UUID_WIKI_LINK; import static org.thingsboard.server.controller.EdgeController.EDGE_ID; @RestController @@ -102,8 +130,9 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Get Device (getDeviceById)", notes = "Fetch the Device 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.") + "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 @@ -121,7 +150,8 @@ 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) + "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 @@ -137,11 +167,13 @@ 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.") + notes = "Create or update the Device. When creating device, platform generates Device Id as " + UUID_WIKI_LINK + + "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 @@ -161,7 +193,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) { @@ -169,14 +201,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 " + UUID_WIKI_LINK + + "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) { @@ -185,7 +248,7 @@ 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.") + 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) @@ -217,7 +280,7 @@ 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.") + 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 @@ -253,7 +316,7 @@ 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.") + 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 @@ -289,7 +352,7 @@ 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.") + "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 @@ -316,7 +379,7 @@ 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.") + 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 @@ -342,7 +405,7 @@ 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'.") + "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 @@ -371,7 +434,7 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Get Tenant Devices (getTenantDevices)", notes = "Returns a page of devices owned by tenant. " + - PAGE_DATA_PARAMETERS) + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody @@ -403,7 +466,7 @@ 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) + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody @@ -414,7 +477,7 @@ public class DeviceController extends BaseController { @RequestParam int page, @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, - @ApiParam(value = DEVICE_PROFILE_ID_DESCRIPTION) + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(required = false) String deviceProfileId, @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @@ -441,7 +504,7 @@ 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.") + "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 @@ -458,13 +521,13 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Get Customer Devices (getCustomerDevices)", notes = "Returns a page of devices objects assigned to customer. " + - PAGE_DATA_PARAMETERS) + 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( @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true) - @PathVariable("customerId") String strCustomerId, + @PathVariable(CUSTOMER_ID) String strCustomerId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -495,7 +558,7 @@ 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) + 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 @@ -508,7 +571,7 @@ public class DeviceController extends BaseController { @RequestParam int page, @ApiParam(value = DEVICE_TYPE_DESCRIPTION) @RequestParam(required = false) String type, - @ApiParam(value = DEVICE_PROFILE_ID_DESCRIPTION) + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION) @RequestParam(required = false) String deviceProfileId, @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @@ -536,7 +599,7 @@ 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. ") + 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 @@ -567,11 +630,13 @@ 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.") + "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()); @@ -593,7 +658,8 @@ 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.") + 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 @@ -614,7 +680,7 @@ public class DeviceController extends BaseController { "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.") + "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 @@ -672,7 +738,8 @@ 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.") + 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) @@ -726,7 +793,7 @@ public class DeviceController extends BaseController { } @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.") + 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 @@ -781,8 +848,11 @@ public class DeviceController extends BaseController { @ApiOperation(value = "Assign device to edge (assignDeviceToEdge)", notes = "Creates assignment of an existing device to an instance of The Edge. " + - "The Edge is a software product for edge computing. " + - "It allows bringing data analysis and management to the edge, while seamlessly synchronizing with the platform server (cloud). ") + 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 @@ -820,7 +890,12 @@ public class DeviceController extends BaseController { } @ApiOperation(value = "Unassign device from edge (unassignDeviceFromEdge)", - notes = "Clears assignment of the device to the edge") + 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 @@ -859,7 +934,7 @@ 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) + 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 @@ -916,7 +991,7 @@ public class DeviceController extends BaseController { 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") + "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 @@ -937,12 +1012,13 @@ 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.") + 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 3a68551773..c1c8743101 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,44 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @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 +96,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 +115,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 +130,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 +158,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 +186,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 " + UUID_WIKI_LINK + + "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()); @@ -180,10 +242,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)); @@ -207,10 +275,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)); @@ -238,14 +311,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)); @@ -254,15 +337,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..eec1098fbd 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,23 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @RestController @TbCoreComponent @Slf4j @@ -75,7 +95,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 +107,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 +128,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 +149,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 " + UUID_WIKI_LINK + + "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 +181,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 +191,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 +201,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 +235,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 +260,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 +300,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 +338,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 +371,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 +403,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 +436,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,11 +453,17 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Set root rule chain for provided edge (setEdgeRootRuleChain)", + 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, - @PathVariable("ruleChainId") String strRuleChainId) throws ThingsboardException { + public Edge setEdgeRootRuleChain(@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); try { @@ -394,16 +491,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 +536,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 +581,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 +617,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 +653,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 +671,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 +694,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 +712,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 +726,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); } @@ -597,6 +737,9 @@ public class EdgeController extends BaseController { edge.setEdgeLicenseKey(null); } + @ApiOperation(value = "Check edge license (checkInstance)", + notes = "Checks license request from edge service by forwarding request to license portal.", + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/license/checkInstance", method = RequestMethod.POST) @ResponseBody public ResponseEntity checkInstance(@RequestBody JsonNode request) throws ThingsboardException { @@ -609,6 +752,9 @@ public class EdgeController extends BaseController { } } + @ApiOperation(value = "Activate edge instance (activateInstance)", + notes = "Activates edge license on license portal.", + produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/license/activateInstance", params = {"licenseSecret", "releaseDate"}, method = RequestMethod.POST) @ResponseBody public ResponseEntity activateInstance(@RequestParam String licenseSecret, 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 6802130338..3b0c32af29 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; @@ -72,6 +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.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; /** @@ -88,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); @@ -100,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)); @@ -113,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()); @@ -350,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)); @@ -375,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(); @@ -388,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 { @@ -419,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)); @@ -448,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)); @@ -475,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 { @@ -502,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(); @@ -526,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(); @@ -549,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()); @@ -573,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 @@ -587,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)); @@ -611,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 @@ -641,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 64537d9f5f..35131de2f7 100644 --- a/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java +++ b/application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java @@ -16,7 +16,10 @@ 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; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -25,26 +28,39 @@ 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; import org.thingsboard.server.queue.util.TbCoreComponent; -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 @RequestMapping("/api") public class Lwm2mController extends BaseController { + @Autowired + private DeviceController deviceController; + + 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 lwM2MServerSecurityInfoRepository.getServerSecurityInfo(bootstrapServer); } catch (Exception e) { @@ -52,28 +68,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..71fdcae1c8 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,23 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @Slf4j @RestController @TbCoreComponent @@ -56,10 +75,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 +102,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 +120,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 +138,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 " + UUID_WIKI_LINK + + "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 +166,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 +218,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 +243,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 +275,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..2f9e325c8f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -20,13 +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; import org.springframework.web.bind.annotation.RequestMapping; @@ -41,6 +42,7 @@ import org.thingsboard.server.actors.tenant.DebugTbRateLimits; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -58,7 +60,9 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; @@ -66,19 +70,46 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.install.InstallScripts; +import org.thingsboard.server.service.rule.TbRuleChainService; import org.thingsboard.server.service.script.JsInvokeService; import org.thingsboard.server.service.script.RuleNodeJsScriptEngine; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; 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_ID; +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.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.RULE_NODE_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.UUID_WIKI_LINK; + @Slf4j @RestController @TbCoreComponent @@ -93,6 +124,28 @@ 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 + protected TbRuleChainService tbRuleChainService; + @Autowired private InstallScripts installScripts; @@ -108,10 +161,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 +178,52 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Get Rule Chain output labels (getRuleChainOutputLabels)", + notes = "Fetch the unique labels for the \"output\" Rule Nodes that belong to the Rule Chain based on the provided Rule Chain Id. " + + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels", method = RequestMethod.GET) + @ResponseBody + public Set getRuleChainOutputLabels( + @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)); + checkRuleChain(ruleChainId, Operation.READ); + return tbRuleChainService.getRuleChainOutputLabels(getTenantId(), ruleChainId); + } catch (Exception e) { + throw handleException(e); + } + } + + @ApiOperation(value = "Get output labels usage (getRuleChainOutputLabelsUsage)", + notes = "Fetch the list of rule chains and the relation types (labels) they use to process output of the current rule chain based on the provided Rule Chain Id. " + + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/ruleChain/{ruleChainId}/output/labels/usage", method = RequestMethod.GET) + @ResponseBody + public List getRuleChainOutputLabelsUsage( + @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)); + checkRuleChain(ruleChainId, Operation.READ); + return tbRuleChainService.getOutputLabelUsage(getCurrentUser().getTenantId(), ruleChainId); + } catch (Exception e) { + throw handleException(e); + } + } + + @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 +234,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 " + UUID_WIKI_LINK + + "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 +279,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 +307,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 +350,17 @@ 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, + @ApiParam(value = "Update related rule nodes.") + @RequestParam(value = "updateRelated", required = false, defaultValue = "true") boolean updateRelated + ) throws ThingsboardException { try { TenantId tenantId = getTenantId(); if (debugPerTenantEnabled) { @@ -250,20 +372,36 @@ public class RuleChainController extends BaseController { } RuleChain ruleChain = checkRuleChain(ruleChainMetaData.getRuleChainId(), Operation.WRITE); - checkNotNull(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData) ? true : null); + RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData); + checkNotNull(result.isSuccess() ? true : null); + + List updatedRuleChains; + if (updateRelated && result.isSuccess()) { + updatedRuleChains = tbRuleChainService.updateRelatedRuleChains(tenantId, ruleChainMetaData.getRuleChainId(), result); + } else { + updatedRuleChains = Collections.emptyList(); + } + RuleChainMetaData savedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId())); if (RuleChainType.CORE.equals(ruleChain.getType())) { tbClusterService.broadcastEntityStateChangeEvent(ruleChain.getTenantId(), ruleChain.getId(), ComponentLifecycleEvent.UPDATED); + updatedRuleChains.forEach(updatedRuleChain -> { + tbClusterService.broadcastEntityStateChangeEvent(updatedRuleChain.getTenantId(), updatedRuleChain.getId(), ComponentLifecycleEvent.UPDATED); + }); } - logEntityAction(ruleChain.getId(), ruleChain, - null, - ActionType.UPDATED, null, ruleChainMetaData); + logEntityAction(ruleChain.getId(), ruleChain, null, ActionType.UPDATED, null, ruleChainMetaData); + for (RuleChain updatedRuleChain : updatedRuleChains) { + RuleChainMetaData updatedRuleChainMetaData = checkNotNull(ruleChainService.loadRuleChainMetaData(tenantId, updatedRuleChain.getId())); + logEntityAction(updatedRuleChain.getId(), updatedRuleChain, null, ActionType.UPDATED, null, updatedRuleChainMetaData); + } if (RuleChainType.EDGE.equals(ruleChain.getType())) { - sendEntityNotificationMsg(ruleChain.getTenantId(), - ruleChain.getId(), EdgeEventActionType.UPDATED); + sendEntityNotificationMsg(ruleChain.getTenantId(), ruleChain.getId(), EdgeEventActionType.UPDATED); + updatedRuleChains.forEach(updatedRuleChain -> { + sendEntityNotificationMsg(updatedRuleChain.getTenantId(), updatedRuleChain.getId(), EdgeEventActionType.UPDATED); + }); } return savedRuleChainMetaData; @@ -276,15 +414,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 +446,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 +497,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_NODE_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 +528,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 +596,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 +612,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 +666,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 +707,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 @@ -559,17 +746,25 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Get Edge Rule Chains (getEdgeRuleChains)", + notes = "Returns a page of Rule Chains assigned to the specified edge. " + RULE_CHAIN_DESCRIPTION + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/edge/{edgeId}/ruleChains", params = {"pageSize", "page"}, method = RequestMethod.GET) @ResponseBody public PageData getEdgeRuleChains( - @PathVariable("edgeId") String strEdgeId, + @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 = 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 { - checkParameter("edgeId", strEdgeId); + checkParameter(EDGE_ID, strEdgeId); try { TenantId tenantId = getCurrentUser().getTenantId(); EdgeId edgeId = new EdgeId(toUUID(strEdgeId)); @@ -581,10 +776,14 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Set Edge Template Root Rule Chain (setEdgeTemplateRootRuleChain)", + notes = "Makes the rule chain to be root rule chain for any new edge that will be created. " + + "Does not update root rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/edgeTemplateRoot", method = RequestMethod.POST) @ResponseBody - public RuleChain setEdgeTemplateRootRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public RuleChain setEdgeTemplateRootRuleChain(@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)); @@ -600,10 +799,14 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Set Auto Assign To Edge Rule Chain (setAutoAssignToEdgeRuleChain)", + notes = "Makes the rule chain to be automatically assigned for any new edge that will be created. " + + "Does not assign this rule chain for already created edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.POST) @ResponseBody - public RuleChain setAutoAssignToEdgeRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public RuleChain setAutoAssignToEdgeRuleChain(@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)); @@ -619,10 +822,14 @@ public class RuleChainController extends BaseController { } } + @ApiOperation(value = "Unset Auto Assign To Edge Rule Chain (unsetAutoAssignToEdgeRuleChain)", + notes = "Removes the rule chain from the list of rule chains that are going to be automatically assigned for any new edge that will be created. " + + "Does not unassign this rule chain for already assigned edges. " + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}/autoAssignToEdge", method = RequestMethod.DELETE) @ResponseBody - public RuleChain unsetAutoAssignToEdgeRuleChain(@PathVariable(RULE_CHAIN_ID) String strRuleChainId) throws ThingsboardException { + public RuleChain unsetAutoAssignToEdgeRuleChain(@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)); @@ -639,6 +846,8 @@ public class RuleChainController extends BaseController { } // TODO: @voba refactor this - add new config to edge rule chain to set it as auto-assign + @ApiOperation(value = "Get Auto Assign To Edge Rule Chains (getAutoAssignToEdgeRuleChains)", + notes = "Returns a list of Rule Chains that will be assigned to a newly created edge. " + RULE_CHAIN_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/autoAssignToEdgeRuleChains", method = RequestMethod.GET) @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..4d0c088194 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,23 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @Slf4j @RestController @TbCoreComponent @@ -53,10 +72,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 +95,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 +113,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 +131,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 " + UUID_WIKI_LINK + + "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 +160,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 +189,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 +214,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 +234,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..7a6a1620ae 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; @@ -73,6 +76,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.AccessValidator; @@ -86,7 +90,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 +102,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 +159,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,155 +171,379 @@ 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 { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, this::getAttributeKeysCallback); + @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 { + try { + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, this::getAttributeKeysCallback); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope)); + @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 { + try { + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, + (result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope)); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - SecurityUser user = getCurrentUser(); - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, 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 = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { + try { + SecurityUser user = getCurrentUser(); + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); + } catch (Exception e) { + throw handleException(e); + } } + + @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 { - SecurityUser user = getCurrentUser(); - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, - (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, 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 = 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 { + try { + SecurityUser user = getCurrentUser(); + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, + (result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, - (result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor())); + @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 { + try { + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, + (result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor())); + } catch (Exception e) { + throw handleException(e); + } } + @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)); + try { + SecurityUser user = getCurrentUser(); + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, + (result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes)); + } catch (Exception e) { + throw handleException(e); + } } - + @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) -> { - // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted - Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); - List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy)) - .collect(Collectors.toList()); - - Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); - }); + try { + return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr, + (result, tenantId, entityId) -> { + // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted + Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); + List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy)) + .collect(Collectors.toList()); + + Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor()); + }); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return saveAttributes(getTenantId(), entityId, scope, request); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); + return saveAttributes(getTenantId(), entityId, scope, request); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveAttributes(getTenantId(), entityId, scope, request); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveAttributes(getTenantId(), entityId, scope, request); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveAttributes(getTenantId(), entityId, scope, request); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveAttributes(getTenantId(), entityId, scope, request); + } catch (Exception e) { + throw handleException(e); + } } + + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveTelemetry(getTenantId(), entityId, requestBody, 0L); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveTelemetry(getTenantId(), entityId, requestBody, 0L); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return saveTelemetry(getTenantId(), entityId, requestBody, ttl); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return saveTelemetry(getTenantId(), entityId, requestBody, ttl); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return deleteTimeseries(entityId, keysStr, deleteAllDataForKeys, startTs, endTs, rewriteLatestIfDeleted); + } catch (Exception e) { + throw handleException(e); + } } private DeferredResult deleteTimeseries(EntityId entityIdStr, String keysStr, boolean deleteAllDataForKeys, @@ -311,9 +573,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,24 +590,59 @@ 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 { - EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); - return deleteAttributes(entityId, scope, keysStr); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); + return deleteAttributes(entityId, scope, keysStr); + } catch (Exception e) { + throw handleException(e); + } } + @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 { - EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); - return deleteAttributes(entityId, scope, keysStr); + 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 { + try { + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); + return deleteAttributes(entityId, scope, keysStr); + } catch (Exception e) { + throw handleException(e); + } } private DeferredResult deleteAttributes(EntityId entityIdSrc, String scope, String keysStr) throws ThingsboardException { @@ -392,6 +688,7 @@ public class TelemetryController extends BaseController { } if (json.isObject()) { List attributes = extractRequestAttributes(json); + attributes.forEach(ConstraintValidator::validateFields); if (attributes.isEmpty()) { return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); } @@ -517,7 +814,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 +849,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 +869,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 +989,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 3459d61c8b..0cfc8f3865 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,48 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @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 { + 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,10 +93,15 @@ 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 { + 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)); @@ -83,10 +111,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 " + UUID_WIKI_LINK + + "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 +144,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 +165,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 +188,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..01023edf6d 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,37 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @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 +80,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 +97,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 +110,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 " + UUID_WIKI_LINK + + "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 +189,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 +208,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 +227,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 +250,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..e83bc8970c 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,27 @@ 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; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; + @RequiredArgsConstructor @RestController @TbCoreComponent @@ -82,20 +103,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 +133,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 +144,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 +168,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 " + UUID_WIKI_LINK + + "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 +230,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 +259,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 +288,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 +321,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 +351,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 +379,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 +409,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..b7f8b28b28 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,31 @@ 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.UUID_WIKI_LINK; +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 +77,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 " + UUID_WIKI_LINK + + "The newly created Widget Type Id will be present in the response. " + + "Specify existing Widget Type id to update the Widget Type. " + + "Referencing non-existing Widget Type Id will cause 'Not Found' error." + + "\n\nWidget Type alias is unique in the scope of Widget Bundle. " + + "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 +111,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 +132,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 +155,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 +178,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 +201,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..36999950c1 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,34 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.List; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; +import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK; +import static org.thingsboard.server.controller.ControllerConstants.WIDGET_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 +78,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 " + UUID_WIKI_LINK + + "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 +112,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 +133,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 +163,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/exception/ThingsboardCredentialsExpiredResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java index 1d2b78fae4..9dc95a3a05 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardCredentialsExpiredResponse.java @@ -15,9 +15,12 @@ */ package org.thingsboard.server.exception; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.springframework.http.HttpStatus; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +@ApiModel public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorResponse { private final String resetToken; @@ -31,6 +34,7 @@ public class ThingsboardCredentialsExpiredResponse extends ThingsboardErrorRespo return new ThingsboardCredentialsExpiredResponse(message, resetToken); } + @ApiModelProperty(position = 5, value = "Password reset token", readOnly = true) public String getResetToken() { return resetToken; } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java index 13d7b1e4a8..05df631f2c 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponse.java @@ -15,11 +15,14 @@ */ package org.thingsboard.server.exception; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import org.springframework.http.HttpStatus; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import java.util.Date; +@ApiModel public class ThingsboardErrorResponse { // HTTP Response Status Code private final HttpStatus status; @@ -43,18 +46,35 @@ public class ThingsboardErrorResponse { return new ThingsboardErrorResponse(message, errorCode, status); } + @ApiModelProperty(position = 1, value = "HTTP Response Status Code", example = "401", readOnly = true) public Integer getStatus() { return status.value(); } + @ApiModelProperty(position = 2, value = "Error message", example = "Authentication failed", readOnly = true) public String getMessage() { return message; } + @ApiModelProperty(position = 3, value = "Platform error code:" + + "\n* `2` - General error (HTTP: 500 - Internal Server Error)" + + "\n\n* `10` - Authentication failed (HTTP: 401 - Unauthorized)" + + "\n\n* `11` - JWT token expired (HTTP: 401 - Unauthorized)" + + "\n\n* `15` - Credentials expired (HTTP: 401 - Unauthorized)" + + "\n\n* `20` - Permission denied (HTTP: 403 - Forbidden)" + + "\n\n* `30` - Invalid arguments (HTTP: 400 - Bad Request)" + + "\n\n* `31` - Bad request params (HTTP: 400 - Bad Request)" + + "\n\n* `32` - Item not found (HTTP: 404 - Not Found)" + + "\n\n* `33` - Too many requests (HTTP: 429 - Too Many Requests)" + + "\n\n* `34` - Too many updates (Too many updates over Websocket session)" + + "\n\n* `40` - Subscription violation (HTTP: 403 - Forbidden)", + example = "10", dataType = "integer", + readOnly = true) public ThingsboardErrorCode getErrorCode() { return errorCode; } + @ApiModelProperty(position = 4, value = "Timestamp", readOnly = true) public Date getTimestamp() { return timestamp; } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 906e87d608..36657e9a91 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -17,9 +17,13 @@ package org.thingsboard.server.exception; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.DisabledException; @@ -30,7 +34,9 @@ import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import org.springframework.web.util.WebUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; @@ -42,11 +48,49 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; @Slf4j @RestControllerAdvice public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHandler implements AccessDeniedHandler { + private static final Map statusToErrorCodeMap = new HashMap<>(); + static { + statusToErrorCodeMap.put(HttpStatus.BAD_REQUEST, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + statusToErrorCodeMap.put(HttpStatus.UNAUTHORIZED, ThingsboardErrorCode.AUTHENTICATION); + statusToErrorCodeMap.put(HttpStatus.FORBIDDEN, ThingsboardErrorCode.PERMISSION_DENIED); + statusToErrorCodeMap.put(HttpStatus.NOT_FOUND, ThingsboardErrorCode.ITEM_NOT_FOUND); + statusToErrorCodeMap.put(HttpStatus.METHOD_NOT_ALLOWED, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + statusToErrorCodeMap.put(HttpStatus.NOT_ACCEPTABLE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + statusToErrorCodeMap.put(HttpStatus.UNSUPPORTED_MEDIA_TYPE, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + statusToErrorCodeMap.put(HttpStatus.TOO_MANY_REQUESTS, ThingsboardErrorCode.TOO_MANY_REQUESTS); + statusToErrorCodeMap.put(HttpStatus.INTERNAL_SERVER_ERROR, ThingsboardErrorCode.GENERAL); + statusToErrorCodeMap.put(HttpStatus.SERVICE_UNAVAILABLE, ThingsboardErrorCode.GENERAL); + } + private static final Map errorCodeToStatusMap = new HashMap<>(); + static { + errorCodeToStatusMap.put(ThingsboardErrorCode.GENERAL, HttpStatus.INTERNAL_SERVER_ERROR); + errorCodeToStatusMap.put(ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED); + errorCodeToStatusMap.put(ThingsboardErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED); + errorCodeToStatusMap.put(ThingsboardErrorCode.CREDENTIALS_EXPIRED, HttpStatus.UNAUTHORIZED); + errorCodeToStatusMap.put(ThingsboardErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN); + errorCodeToStatusMap.put(ThingsboardErrorCode.INVALID_ARGUMENTS, HttpStatus.BAD_REQUEST); + errorCodeToStatusMap.put(ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST); + errorCodeToStatusMap.put(ThingsboardErrorCode.ITEM_NOT_FOUND, HttpStatus.NOT_FOUND); + errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_REQUESTS, HttpStatus.TOO_MANY_REQUESTS); + errorCodeToStatusMap.put(ThingsboardErrorCode.TOO_MANY_UPDATES, HttpStatus.TOO_MANY_REQUESTS); + errorCodeToStatusMap.put(ThingsboardErrorCode.SUBSCRIPTION_VIOLATION, HttpStatus.FORBIDDEN); + } + + private static ThingsboardErrorCode statusToErrorCode(HttpStatus status) { + return statusToErrorCodeMap.getOrDefault(status, ThingsboardErrorCode.GENERAL); + } + + private static HttpStatus errorCodeToStatus(ThingsboardErrorCode errorCode) { + return errorCodeToStatusMap.getOrDefault(errorCode, HttpStatus.INTERNAL_SERVER_ERROR); + } + @Autowired private ObjectMapper mapper; @@ -95,36 +139,22 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand } } + @NotNull + @Override + protected ResponseEntity handleExceptionInternal( + @NotNull Exception ex, @Nullable Object body, + @NotNull HttpHeaders headers, @NotNull HttpStatus status, + @NotNull WebRequest request) { + if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { + request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); + } + ThingsboardErrorCode errorCode = statusToErrorCode(status); + return new ResponseEntity<>(ThingsboardErrorResponse.of(ex.getMessage(), errorCode, status), headers, status); + } private void handleThingsboardException(ThingsboardException thingsboardException, HttpServletResponse response) throws IOException { - ThingsboardErrorCode errorCode = thingsboardException.getErrorCode(); - HttpStatus status; - - switch (errorCode) { - case AUTHENTICATION: - status = HttpStatus.UNAUTHORIZED; - break; - case PERMISSION_DENIED: - status = HttpStatus.FORBIDDEN; - break; - case INVALID_ARGUMENTS: - status = HttpStatus.BAD_REQUEST; - break; - case ITEM_NOT_FOUND: - status = HttpStatus.NOT_FOUND; - break; - case BAD_REQUEST_PARAMS: - status = HttpStatus.BAD_REQUEST; - break; - case GENERAL: - status = HttpStatus.INTERNAL_SERVER_ERROR; - break; - default: - status = HttpStatus.INTERNAL_SERVER_ERROR; - break; - } - + HttpStatus status = errorCodeToStatus(errorCode); response.setStatus(status.value()); mapper.writeValue(response.getWriter(), ThingsboardErrorResponse.of(thingsboardException.getMessage(), errorCode, status)); } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index d66810093f..02bc87aaf9 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -31,6 +31,7 @@ import org.thingsboard.server.service.install.TsDatabaseSchemaService; import org.thingsboard.server.service.install.TsLatestDatabaseSchemaService; import org.thingsboard.server.service.install.migrate.EntitiesMigrateService; import org.thingsboard.server.service.install.migrate.TsLatestMigrateService; +import org.thingsboard.server.service.install.update.CacheCleanupService; import org.thingsboard.server.service.install.update.DataUpdateService; @Service @@ -74,6 +75,9 @@ public class ThingsboardInstallService { @Autowired private DataUpdateService dataUpdateService; + @Autowired + private CacheCleanupService cacheCleanupService; + @Autowired(required = false) private EntitiesMigrateService entitiesMigrateService; @@ -85,6 +89,8 @@ public class ThingsboardInstallService { if (isUpgrade) { log.info("Starting ThingsBoard Upgrade from version {} ...", upgradeFromVersion); + cacheCleanupService.clearCache(upgradeFromVersion); + if ("2.5.0-cassandra".equals(upgradeFromVersion)) { log.info("Migrating ThingsBoard entities data from cassandra to SQL database ..."); entitiesMigrateService.migrate(); @@ -204,9 +210,16 @@ public class ThingsboardInstallService { log.info("Upgrading ThingsBoard from version 3.3.0 to 3.3.1 ..."); case "3.3.1": log.info("Upgrading ThingsBoard from version 3.3.1 to 3.3.2 ..."); + case "3.3.2": + log.info("Upgrading ThingsBoard from version 3.3.2 to 3.3.3 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.3.2"); + dataUpdateService.updateData("3.3.2"); log.info("Updating system data..."); systemDataLoaderService.updateSystemWidgets(); break; + + //TODO update CacheCleanupService on the next version upgrade + default: throw new RuntimeException("Unable to upgrade ThingsBoard, unsupported fromVersion: " + upgradeFromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java index 2f94ac3b0d..f67b0617ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java @@ -17,78 +17,69 @@ package org.thingsboard.server.service.asset; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; -import org.thingsboard.server.service.importing.BulkImportRequest; -import org.thingsboard.server.service.importing.ImportedEntityInfo; -import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.permission.AccessControlService; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.Map; import java.util.Optional; @Service @TbCoreComponent +@RequiredArgsConstructor public class AssetBulkImportService extends AbstractBulkImportService { private final AssetService assetService; - public AssetBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache, - AccessControlService accessControlService, AccessValidator accessValidator, - EntityActionService entityActionService, TbClusterService clusterService, AssetService assetService) { - super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService); - this.assetService = assetService; - } - @Override - protected ImportedEntityInfo saveEntity(BulkImportRequest importRequest, Map fields, SecurityUser user) { - ImportedEntityInfo importedEntityInfo = new ImportedEntityInfo<>(); - - Asset asset = new Asset(); - asset.setTenantId(user.getTenantId()); - setAssetFields(asset, fields); - - Asset existingAsset = assetService.findAssetByTenantIdAndName(user.getTenantId(), asset.getName()); - if (existingAsset != null && importRequest.getMapping().getUpdate()) { - importedEntityInfo.setOldEntity(new Asset(existingAsset)); - importedEntityInfo.setUpdated(true); - existingAsset.update(asset); - asset = existingAsset; - } - asset = assetService.saveAsset(asset); - - importedEntityInfo.setEntity(asset); - return importedEntityInfo; - } - - private void setAssetFields(Asset asset, Map fields) { - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(asset.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); + protected void setEntityFields(Asset entity, Map fields) { + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(entity.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); fields.forEach((columnType, value) -> { switch (columnType) { case NAME: - asset.setName(value); + entity.setName(value); break; case TYPE: - asset.setType(value); + entity.setType(value); break; case LABEL: - asset.setLabel(value); + entity.setLabel(value); break; case DESCRIPTION: additionalInfo.set("description", new TextNode(value)); break; } }); - asset.setAdditionalInfo(additionalInfo); + entity.setAdditionalInfo(additionalInfo); + } + + @Override + protected Asset saveEntity(Asset entity, Map fields) { + return assetService.saveAsset(entity); + } + + @Override + protected Asset findOrCreateEntity(TenantId tenantId, String name) { + return Optional.ofNullable(assetService.findAssetByTenantIdAndName(tenantId, name)) + .orElseGet(Asset::new); + } + + @Override + protected void setOwners(Asset entity, SecurityUser user) { + entity.setTenantId(user.getTenantId()); + entity.setCustomerId(user.getCustomerId()); + } + + @Override + protected EntityType getEntityType() { + return EntityType.ASSET; } } diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java index 00a5bca503..eaacde0874 100644 --- a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java +++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java @@ -152,24 +152,14 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe try { scannedComponent.setType(type); Class clazz = Class.forName(clazzName); - switch (type) { - case ENRICHMENT: - case FILTER: - case TRANSFORMATION: - case ACTION: - case EXTERNAL: - RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); - scannedComponent.setName(ruleNodeAnnotation.name()); - scannedComponent.setScope(ruleNodeAnnotation.scope()); - NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); - ObjectNode configurationDescriptor = mapper.createObjectNode(); - JsonNode node = mapper.valueToTree(nodeDefinition); - configurationDescriptor.set("nodeDefinition", node); - scannedComponent.setConfigurationDescriptor(configurationDescriptor); - break; - default: - throw new RuntimeException(type + " is not supported yet!"); - } + RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); + scannedComponent.setName(ruleNodeAnnotation.name()); + scannedComponent.setScope(ruleNodeAnnotation.scope()); + NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); + ObjectNode configurationDescriptor = mapper.createObjectNode(); + JsonNode node = mapper.valueToTree(nodeDefinition); + configurationDescriptor.set("nodeDefinition", node); + scannedComponent.setConfigurationDescriptor(configurationDescriptor); scannedComponent.setClazz(clazzName); log.info("Processing scanned component: {}", scannedComponent); } catch (Exception e) { @@ -200,6 +190,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled()); nodeDefinition.setRelationTypes(getRelationTypesWithFailureRelation(nodeAnnotation)); nodeDefinition.setCustomRelations(nodeAnnotation.customRelations()); + nodeDefinition.setRuleChainNode(nodeAnnotation.ruleChainNode()); Class configClazz = nodeAnnotation.configClazz(); NodeConfiguration config = configClazz.getDeclaredConstructor().newInstance(); NodeConfiguration defaultConfiguration = config.defaultConfiguration(); 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..353451df3b 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 @@ -18,18 +18,19 @@ package org.thingsboard.server.service.device; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; @@ -45,17 +46,10 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; -import org.thingsboard.server.service.importing.BulkImportRequest; -import org.thingsboard.server.service.importing.ImportedEntityInfo; -import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.permission.AccessControlService; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.Collection; import java.util.EnumSet; @@ -63,71 +57,32 @@ 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 +@RequiredArgsConstructor public class DeviceBulkImportService extends AbstractBulkImportService { protected final DeviceService deviceService; protected final DeviceCredentialsService deviceCredentialsService; protected final DeviceProfileService deviceProfileService; - public DeviceBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache, - AccessControlService accessControlService, AccessValidator accessValidator, - EntityActionService entityActionService, TbClusterService clusterService, - DeviceService deviceService, DeviceCredentialsService deviceCredentialsService, - DeviceProfileService deviceProfileService) { - super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService); - this.deviceService = deviceService; - this.deviceCredentialsService = deviceCredentialsService; - this.deviceProfileService = deviceProfileService; - } + private final Lock findOrCreateDeviceProfileLock = new ReentrantLock(); @Override - protected ImportedEntityInfo saveEntity(BulkImportRequest importRequest, Map fields, SecurityUser user) { - ImportedEntityInfo importedEntityInfo = new ImportedEntityInfo<>(); - - Device device = new Device(); - device.setTenantId(user.getTenantId()); - setDeviceFields(device, fields); - - Device existingDevice = deviceService.findDeviceByTenantIdAndName(user.getTenantId(), device.getName()); - if (existingDevice != null && importRequest.getMapping().getUpdate()) { - importedEntityInfo.setOldEntity(new Device(existingDevice)); - importedEntityInfo.setUpdated(true); - existingDevice.updateDevice(device); - device = existingDevice; - } - - DeviceCredentials deviceCredentials; - try { - deviceCredentials = createDeviceCredentials(fields); - deviceCredentialsService.formatCredentials(deviceCredentials); - } catch (Exception e) { - throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); - } - - if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { - setUpLwM2mDeviceProfile(user.getTenantId(), device); - } - - device = deviceService.saveDeviceWithCredentials(device, deviceCredentials); - - importedEntityInfo.setEntity(device); - return importedEntityInfo; - } - - private void setDeviceFields(Device device, Map fields) { - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(device.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); + protected void setEntityFields(Device entity, Map fields) { + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(entity.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); fields.forEach((columnType, value) -> { switch (columnType) { case NAME: - device.setName(value); + entity.setName(value); break; case TYPE: - device.setType(value); + entity.setType(value); break; case LABEL: - device.setLabel(value); + entity.setLabel(value); break; case DESCRIPTION: additionalInfo.set("description", new TextNode(value)); @@ -136,10 +91,45 @@ public class DeviceBulkImportService extends AbstractBulkImportService { additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value))); break; } - device.setAdditionalInfo(additionalInfo); + entity.setAdditionalInfo(additionalInfo); }); } + @Override + protected Device saveEntity(Device entity, Map fields) { + DeviceCredentials deviceCredentials; + try { + deviceCredentials = createDeviceCredentials(fields); + deviceCredentialsService.formatCredentials(deviceCredentials); + } catch (Exception e) { + throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage()); + } + + DeviceProfile deviceProfile; + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) { + deviceProfile = setUpLwM2mDeviceProfile(entity.getTenantId(), entity); + } else if (StringUtils.isNotEmpty(entity.getType())) { + deviceProfile = deviceProfileService.findOrCreateDeviceProfile(entity.getTenantId(), entity.getType()); + } else { + deviceProfile = deviceProfileService.findDefaultDeviceProfile(entity.getTenantId()); + } + entity.setDeviceProfileId(deviceProfile.getId()); + + return deviceService.saveDeviceWithCredentials(entity, deviceCredentials); + } + + @Override + protected Device findOrCreateEntity(TenantId tenantId, String name) { + return Optional.ofNullable(deviceService.findDeviceByTenantIdAndName(tenantId, name)) + .orElseGet(Device::new); + } + + @Override + protected void setOwners(Device entity, SecurityUser user) { + entity.setTenantId(user.getTenantId()); + entity.setCustomerId(user.getCustomerId()); + } + @SneakyThrows private DeviceCredentials createDeviceCredentials(Map fields) { DeviceCredentials credentials = new DeviceCredentials(); @@ -215,36 +205,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) { @@ -256,4 +253,9 @@ public class DeviceBulkImportService extends AbstractBulkImportService { } } + @Override + protected EntityType getEntityType() { + return EntityType.DEVICE; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java index ec6a2a4e55..3f12d8f6e7 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeBulkImportService.java @@ -17,90 +17,81 @@ package org.thingsboard.server.service.edge; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.server.cluster.TbClusterService; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.action.EntityActionService; import org.thingsboard.server.service.importing.AbstractBulkImportService; import org.thingsboard.server.service.importing.BulkImportColumnType; -import org.thingsboard.server.service.importing.BulkImportRequest; -import org.thingsboard.server.service.importing.ImportedEntityInfo; -import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.permission.AccessControlService; -import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.Map; import java.util.Optional; @Service @TbCoreComponent +@RequiredArgsConstructor public class EdgeBulkImportService extends AbstractBulkImportService { private final EdgeService edgeService; - public EdgeBulkImportService(TelemetrySubscriptionService tsSubscriptionService, TbTenantProfileCache tenantProfileCache, - AccessControlService accessControlService, AccessValidator accessValidator, - EntityActionService entityActionService, TbClusterService clusterService, EdgeService edgeService) { - super(tsSubscriptionService, tenantProfileCache, accessControlService, accessValidator, entityActionService, clusterService); - this.edgeService = edgeService; - } - @Override - protected ImportedEntityInfo saveEntity(BulkImportRequest importRequest, Map fields, SecurityUser user) { - ImportedEntityInfo importedEntityInfo = new ImportedEntityInfo<>(); - - Edge edge = new Edge(); - edge.setTenantId(user.getTenantId()); - setEdgeFields(edge, fields); - - Edge existingEdge = edgeService.findEdgeByTenantIdAndName(user.getTenantId(), edge.getName()); - if (existingEdge != null && importRequest.getMapping().getUpdate()) { - importedEntityInfo.setOldEntity(new Edge(existingEdge)); - importedEntityInfo.setUpdated(true); - existingEdge.update(edge); - edge = existingEdge; - } - edge = edgeService.saveEdge(edge, true); - - importedEntityInfo.setEntity(edge); - return importedEntityInfo; - } - - private void setEdgeFields(Edge edge, Map fields) { - ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(edge.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); + protected void setEntityFields(Edge entity, Map fields) { + ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(entity.getAdditionalInfo()).orElseGet(JacksonUtil::newObjectNode); fields.forEach((columnType, value) -> { switch (columnType) { case NAME: - edge.setName(value); + entity.setName(value); break; case TYPE: - edge.setType(value); + entity.setType(value); break; case LABEL: - edge.setLabel(value); + entity.setLabel(value); break; case DESCRIPTION: additionalInfo.set("description", new TextNode(value)); break; case EDGE_LICENSE_KEY: - edge.setEdgeLicenseKey(value); + entity.setEdgeLicenseKey(value); break; case CLOUD_ENDPOINT: - edge.setCloudEndpoint(value); + entity.setCloudEndpoint(value); break; case ROUTING_KEY: - edge.setRoutingKey(value); + entity.setRoutingKey(value); break; case SECRET: - edge.setSecret(value); + entity.setSecret(value); break; } }); - edge.setAdditionalInfo(additionalInfo); + entity.setAdditionalInfo(additionalInfo); + } + + @Override + protected Edge saveEntity(Edge entity, Map fields) { + return edgeService.saveEdge(entity, true); + } + + @Override + protected Edge findOrCreateEntity(TenantId tenantId, String name) { + return Optional.ofNullable(edgeService.findEdgeByTenantIdAndName(tenantId, name)) + .orElseGet(Edge::new); + } + + @Override + protected void setOwners(Edge entity, SecurityUser user) { + entity.setTenantId(user.getTenantId()); + entity.setCustomerId(user.getCustomerId()); + } + + @Override + protected EntityType getEntityType() { + return EntityType.EDGE; } } 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..d1eef6ec17 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 @@ -19,14 +19,21 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; -import org.thingsboard.server.cluster.TbClusterService; -import org.thingsboard.server.common.data.BaseData; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.beans.factory.annotation.Autowired; +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.common.data.EntityType; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -42,73 +49,123 @@ import org.thingsboard.server.service.security.AccessValidator; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.AccessControlService; import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; 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; import java.util.stream.Collectors; import java.util.stream.Stream; -@RequiredArgsConstructor -public abstract class AbstractBulkImportService> { - protected final TelemetrySubscriptionService tsSubscriptionService; - protected final TbTenantProfileCache tenantProfileCache; - protected final AccessControlService accessControlService; - protected final AccessValidator accessValidator; - protected final EntityActionService entityActionService; - protected final TbClusterService clusterService; +public abstract class AbstractBulkImportService & HasTenantId> { + @Autowired + private TelemetrySubscriptionService tsSubscriptionService; + @Autowired + private TbTenantProfileCache tenantProfileCache; + @Autowired + private AccessControlService accessControlService; + @Autowired + private AccessValidator accessValidator; + @Autowired + private EntityActionService entityActionService; - public final BulkImportResult 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(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; } - protected abstract ImportedEntityInfo saveEntity(BulkImportRequest importRequest, Map fields, SecurityUser user); + @SneakyThrows + private ImportedEntityInfo saveEntity(Map fields, SecurityUser user) { + ImportedEntityInfo importedEntityInfo = new ImportedEntityInfo<>(); + + E entity = findOrCreateEntity(user.getTenantId(), fields.get(BulkImportColumnType.NAME)); + if (entity.getId() != null) { + importedEntityInfo.setOldEntity((E) entity.getClass().getConstructor(entity.getClass()).newInstance(entity)); + importedEntityInfo.setUpdated(true); + } else { + setOwners(entity, user); + } + + setEntityFields(entity, fields); + accessControlService.checkPermission(user, Resource.of(getEntityType()), Operation.WRITE, entity.getId(), entity); + + E savedEntity = saveEntity(entity, fields); + + importedEntityInfo.setEntity(savedEntity); + return importedEntityInfo; + } + + + protected abstract E findOrCreateEntity(TenantId tenantId, String name); + + protected abstract void setOwners(E entity, SecurityUser user); + + protected abstract void setEntityFields(E entity, Map fields); + + protected abstract E saveEntity(E entity, Map fields); + + protected abstract EntityType getEntityType(); + - /* - * Attributes' values are firstly added to JsonObject in order to then make some type cast, - * because we get all values as strings from CSV - * */ private void saveKvs(SecurityUser user, E entity, Map data) { Arrays.stream(BulkImportColumnType.values()) .filter(BulkImportColumnType::isKv) @@ -186,8 +243,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 +265,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/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index fcb9632020..27e8375dc8 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -43,6 +43,7 @@ import java.sql.SQLSyntaxErrorException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.List; +import java.util.concurrent.TimeUnit; import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO; import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS; @@ -469,6 +470,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.3.2": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + log.info("Updating schema settings..."); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003003;"); + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } @@ -476,10 +489,25 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService private void loadSql(Path sqlFile, Connection conn) throws Exception { String sql = new String(Files.readAllBytes(sqlFile), Charset.forName("UTF-8")); - conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + Statement st = conn.createStatement(); + st.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(3)); + st.execute(sql);//NOSONAR, ignoring because method used to execute thingsboard database upgrade script + printWarnings(st); Thread.sleep(5000); } + protected void printWarnings(Statement statement) throws SQLException { + SQLWarning warnings = statement.getWarnings(); + if (warnings != null) { + log.info("{}", warnings.getMessage()); + SQLWarning nextWarning = warnings.getNextWarning(); + while (nextWarning != null) { + log.info("{}", nextWarning.getMessage()); + nextWarning = nextWarning.getNextWarning(); + } + } + } + protected boolean isOldSchema(Connection conn, long fromVersion) { boolean isOldSchema = true; try { diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java new file mode 100644 index 0000000000..14a8bddc67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/update/CacheCleanupService.java @@ -0,0 +1,22 @@ +/** + * 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.install.update; + +public interface CacheCleanupService { + + void clearCache(String fromVersion) throws Exception; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java new file mode 100644 index 0000000000..1456b3f975 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultCacheCleanupService.java @@ -0,0 +1,75 @@ +/** + * 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.install.update; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@RequiredArgsConstructor +@Service +@Profile("install") +@Slf4j +public class DefaultCacheCleanupService implements CacheCleanupService { + + private final CacheManager cacheManager; + + /** + * Cleanup caches that can not deserialize anymore due to schema upgrade or data update using sql scripts. + * Refer to SqlDatabaseUpgradeService and /data/upgrage/*.sql + * to discover which tables were changed + * */ + @Override + public void clearCache(String fromVersion) throws Exception { + switch (fromVersion) { + case "3.0.1": + log.info("Clear cache to upgrade from version 3.0.1 to 3.1.0 ..."); + clearAllCaches(); + //do not break to show explicit calls for next versions + case "3.1.1": + log.info("Clear cache to upgrade from version 3.1.1 to 3.2.0 ..."); + clearCacheByName("devices"); + clearCacheByName("deviceProfiles"); + clearCacheByName("tenantProfiles"); + case "3.2.2": + log.info("Clear cache to upgrade from version 3.2.2 to 3.3.0 ..."); + clearCacheByName("devices"); + clearCacheByName("deviceProfiles"); + clearCacheByName("tenantProfiles"); + clearCacheByName("relations"); + break; + default: + //Do nothing, since cache cleanup is optional. + } + } + + void clearAllCaches() { + cacheManager.getCacheNames().forEach(this::clearCacheByName); + } + + void clearCacheByName(final String cacheName) { + Cache cache = cacheManager.getCache(cacheName); + Objects.requireNonNull(cache, "Cache does not exist for name " + cacheName); + cache.clear(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index ebe727b6da..de8216a4ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -25,6 +25,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.flow.TbRuleChainInputNode; +import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; import org.thingsboard.rule.engine.profile.TbDeviceProfileNode; import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration; import org.thingsboard.server.common.data.EntityView; @@ -34,6 +36,8 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; @@ -43,8 +47,11 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmDao; @@ -52,7 +59,9 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; +import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.oauth2.OAuth2Service; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.sql.device.DeviceProfileRepository; import org.thingsboard.server.dao.tenant.TenantService; @@ -63,6 +72,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -75,6 +85,9 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private TenantService tenantService; + @Autowired + private RelationService relationService; + @Autowired private RuleChainService ruleChainService; @@ -124,6 +137,10 @@ public class DefaultDataUpdateService implements DataUpdateService { deviceProfileEntityDynamicConditionsUpdater.updateEntities(null); updateOAuth2Params(); break; + case "3.3.2": + log.info("Updating data from version 3.3.2 to 3.3.3 ..."); + updateNestedRuleChains(); + break; default: throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion); } @@ -144,29 +161,38 @@ public class DefaultDataUpdateService implements DataUpdateService { @Override protected void updateEntity(DeviceProfileEntity deviceProfile) { - if (deviceProfile.getProfileData().has("alarms") && - !deviceProfile.getProfileData().get("alarms").isNull()) { - boolean isUpdated = false; - JsonNode array = deviceProfile.getProfileData().get("alarms"); - for (JsonNode node : array) { - if (node.has("createRules")) { - JsonNode createRules = node.get("createRules"); - for (AlarmSeverity severity : AlarmSeverity.values()) { - if (createRules.has(severity.name())) { - isUpdated = isUpdated || convertDeviceProfileAlarmRulesForVersion330(createRules.get(severity.name()).get("condition").get("spec")); - } - } - } - if (node.has("clearRule") && !node.get("clearRule").isNull()) { - isUpdated = isUpdated || convertDeviceProfileAlarmRulesForVersion330(node.get("clearRule").get("condition").get("spec")); + if (convertDeviceProfileForVersion330(deviceProfile.getProfileData())) { + deviceProfileRepository.save(deviceProfile); + } + } + }; + + boolean convertDeviceProfileForVersion330(JsonNode profileData) { + boolean isUpdated = false; + if (profileData.has("alarms") && !profileData.get("alarms").isNull()) { + JsonNode alarms = profileData.get("alarms"); + for (JsonNode alarm : alarms) { + if (alarm.has("createRules")) { + JsonNode createRules = alarm.get("createRules"); + for (AlarmSeverity severity : AlarmSeverity.values()) { + if (createRules.has(severity.name())) { + JsonNode spec = createRules.get(severity.name()).get("condition").get("spec"); + if (convertDeviceProfileAlarmRulesForVersion330(spec)) { + isUpdated = true; } } - if (isUpdated) { - deviceProfileRepository.save(deviceProfile); - } } } - }; + if (alarm.has("clearRule") && !alarm.get("clearRule").isNull()) { + JsonNode spec = alarm.get("clearRule").get("condition").get("spec"); + if (convertDeviceProfileAlarmRulesForVersion330(spec)) { + isUpdated = true; + } + } + } + } + return isUpdated; + } private final PaginatedUpdater tenantsDefaultRuleChainUpdater = new PaginatedUpdater<>() { @@ -199,6 +225,74 @@ public class DefaultDataUpdateService implements DataUpdateService { } }; + private void updateNestedRuleChains() { + try { + var packSize = 1024; + var updated = 0; + boolean hasNext = true; + while (hasNext) { + List relations = relationService.findRuleNodeToRuleChainRelations(TenantId.SYS_TENANT_ID, RuleChainType.CORE, packSize); + hasNext = relations.size() == packSize; + for (EntityRelation relation : relations) { + try { + RuleNodeId sourceNodeId = new RuleNodeId(relation.getFrom().getId()); + RuleNode sourceNode = ruleChainService.findRuleNodeById(TenantId.SYS_TENANT_ID, sourceNodeId); + if (sourceNode == null) { + log.info("Skip processing of relation for non existing source rule node: [{}]", sourceNodeId); + relationService.deleteRelation(TenantId.SYS_TENANT_ID, relation); + continue; + } + RuleChainId sourceRuleChainId = sourceNode.getRuleChainId(); + RuleChainId targetRuleChainId = new RuleChainId(relation.getTo().getId()); + RuleChain targetRuleChain = ruleChainService.findRuleChainById(TenantId.SYS_TENANT_ID, targetRuleChainId); + if (targetRuleChain == null) { + log.info("Skip processing of relation for non existing target rule chain: [{}]", targetRuleChainId); + relationService.deleteRelation(TenantId.SYS_TENANT_ID, relation); + continue; + } + TenantId tenantId = targetRuleChain.getTenantId(); + RuleNode targetNode = new RuleNode(); + targetNode.setName(targetRuleChain.getName()); + targetNode.setRuleChainId(sourceRuleChainId); + targetNode.setType(TbRuleChainInputNode.class.getName()); + TbRuleChainInputNodeConfiguration configuration = new TbRuleChainInputNodeConfiguration(); + configuration.setRuleChainId(targetRuleChain.getId().toString()); + targetNode.setConfiguration(JacksonUtil.valueToTree(configuration)); + targetNode.setAdditionalInfo(relation.getAdditionalInfo()); + targetNode.setDebugMode(false); + targetNode = ruleChainService.saveRuleNode(tenantId, targetNode); + + EntityRelation sourceRuleChainToRuleNode = new EntityRelation(); + sourceRuleChainToRuleNode.setFrom(sourceRuleChainId); + sourceRuleChainToRuleNode.setTo(targetNode.getId()); + sourceRuleChainToRuleNode.setType(EntityRelation.CONTAINS_TYPE); + sourceRuleChainToRuleNode.setTypeGroup(RelationTypeGroup.RULE_CHAIN); + relationService.saveRelation(tenantId, sourceRuleChainToRuleNode); + + EntityRelation sourceRuleNodeToTargetRuleNode = new EntityRelation(); + sourceRuleNodeToTargetRuleNode.setFrom(sourceNode.getId()); + sourceRuleNodeToTargetRuleNode.setTo(targetNode.getId()); + sourceRuleNodeToTargetRuleNode.setType(relation.getType()); + sourceRuleNodeToTargetRuleNode.setTypeGroup(RelationTypeGroup.RULE_NODE); + sourceRuleNodeToTargetRuleNode.setAdditionalInfo(relation.getAdditionalInfo()); + relationService.saveRelation(tenantId, sourceRuleNodeToTargetRuleNode); + + //Delete old relation + relationService.deleteRelation(tenantId, relation); + updated++; + } catch (Exception e) { + log.info("Failed to update RuleNodeToRuleChainRelation: {}", relation, e); + } + } + if (updated > 0) { + log.info("RuleNodeToRuleChainRelations: {} entities updated so far...", updated); + } + } + } catch (Exception e) { + log.error("Unable to update Tenant", e); + } + } + private final PaginatedUpdater tenantsDefaultEdgeRuleChainUpdater = new PaginatedUpdater<>() { @@ -376,6 +470,8 @@ public class DefaultDataUpdateService implements DataUpdateService { private final PaginatedUpdater tenantsAlarmsCustomerUpdater = new PaginatedUpdater<>() { + final AtomicLong processed = new AtomicLong(); + @Override protected String getName() { return "Tenants alarms customer updater"; @@ -393,12 +489,12 @@ public class DefaultDataUpdateService implements DataUpdateService { @Override protected void updateEntity(Tenant tenant) { - updateTenantAlarmsCustomer(tenant.getId()); + updateTenantAlarmsCustomer(tenant.getId(), getName(), processed); } }; - private void updateTenantAlarmsCustomer(TenantId tenantId) { - AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(100), null, null, false); + private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) { + AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, false); PageData alarms = alarmDao.findAlarms(tenantId, alarmQuery); boolean hasNext = true; while (hasNext) { @@ -407,6 +503,9 @@ public class DefaultDataUpdateService implements DataUpdateService { alarm.setCustomerId(entityService.fetchEntityCustomerId(tenantId, alarm.getOriginator())); alarmDao.save(tenantId, alarm); } + if (processed.incrementAndGet() % 1000 == 0) { + log.info("{}: {} alarms processed so far...", name, processed); + } } if (alarms.hasNext()) { alarmQuery.setPageLink(alarmQuery.getPageLink().nextPageLink()); @@ -417,7 +516,7 @@ public class DefaultDataUpdateService implements DataUpdateService { } } - private boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) { + boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) { if (spec != null) { if (spec.has("type") && spec.get("type").asText().equals("DURATION")) { if (spec.has("value")) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java index 768d627a16..c10d1b4bd7 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/PaginatedUpdater.java @@ -28,6 +28,7 @@ public abstract class PaginatedUpdater { private int updated = 0; public void updateEntities(I id) { + log.info("{}: started...", getName()); updated = 0; PageLink pageLink = new PageLink(DEFAULT_LIMIT); boolean hasNext = true; diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java index b4ac3d30fd..fcf68bf13a 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java @@ -22,28 +22,11 @@ import org.eclipse.leshan.core.util.Hex; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.transport.lwm2m.config.LwM2MSecureServerConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; -import java.math.BigInteger; -import java.security.AlgorithmParameters; -import java.security.GeneralSecurityException; -import java.security.KeyFactory; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.UnrecoverableKeyException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.X509Certificate; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.KeySpec; - @Slf4j @Service @RequiredArgsConstructor @@ -72,10 +55,9 @@ public class LwM2MServerSecurityInfoRepository { private String getPublicKey(LwM2MSecureServerConfig config) { try { - KeyStore keyStore = serverConfig.getKeyStoreValue(); - if (keyStore != null) { - X509Certificate serverCertificate = (X509Certificate) serverConfig.getKeyStoreValue().getCertificate(config.getCertificateAlias()); - return Hex.encodeHexString(serverCertificate.getPublicKey().getEncoded()); + SslCredentials sslCredentials = config.getSslCredentials(); + if (sslCredentials != null) { + return Hex.encodeHexString(sslCredentials.getPublicKey().getEncoded()); } } catch (Exception e) { log.trace("Failed to fetch public key from key store!", e); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java index f29377445d..a94731696f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.queue.processing; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.gen.transport.TransportProtos; @@ -77,6 +78,7 @@ public class TbRuleEngineProcessingStrategyFactory { @Override public TbRuleEngineProcessingDecision analyze(TbRuleEngineProcessingResult result) { if (result.isSuccess()) { + log.trace("[{}] The result of the msg pack processing is successful, going to proceed with processing of the following msgs", queueName); return new TbRuleEngineProcessingDecision(true, null); } else { if (retryCount == 0) { @@ -91,15 +93,28 @@ public class TbRuleEngineProcessingStrategyFactory { log.debug("[{}] Skip reprocess of the rule engine pack due to max allowed failure percentage", queueName); return new TbRuleEngineProcessingDecision(true, null); } else { + log.debug("[{}] The result of msg pack processing is unsuccessful, checking unprocessed msgs and going to reprocess them", queueName); ConcurrentMap> toReprocess = new ConcurrentHashMap<>(initialTotalCount); if (retryFailed) { result.getFailedMap().forEach(toReprocess::put); + } else if (log.isDebugEnabled() && !result.getFailedMap().isEmpty()) { + log.debug("[{}] Skipped {} failed messages due to the processing strategy configuration", queueName, result.getFailedMap().size()); } if (retryTimeout) { result.getPendingMap().forEach(toReprocess::put); + } else if (log.isDebugEnabled() && !result.getPendingMap().isEmpty()) { + log.debug("[{}] Skipped {} timedOut messages due to the processing strategy configuration", queueName, result.getPendingMap().size()); } if (retrySuccessful) { result.getSuccessMap().forEach(toReprocess::put); + } else if (log.isTraceEnabled() && !result.getSuccessMap().isEmpty()) { + log.trace("[{}] Skipped {} successful messages due to the processing strategy configuration", queueName, result.getSuccessMap().size()); + } + if (CollectionUtils.isEmpty(toReprocess)) { + if (log.isDebugEnabled()) { + log.debug("[{}] Stopping the reprocessing logic due to reprocessing map is empty", queueName); + } + return new TbRuleEngineProcessingDecision(true, null); } log.debug("[{}] Going to reprocess {} messages", queueName, toReprocess.size()); if (log.isTraceEnabled()) { diff --git a/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java new file mode 100644 index 0000000000..e9795ce15f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java @@ -0,0 +1,191 @@ +/** + * 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.rule; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.flow.TbRuleChainInputNode; +import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration; +import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; +import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.rule.RuleChainService; +import org.thingsboard.server.queue.util.TbCoreComponent; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Service +@TbCoreComponent +@Slf4j +public class DefaultTbRuleChainService implements TbRuleChainService { + + private final RuleChainService ruleChainService; + private final RelationService relationService; + + @Override + public Set getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId) { + RuleChainMetaData metaData = ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); + Set outputLabels = new TreeSet<>(); + for (RuleNode ruleNode : metaData.getNodes()) { + if (isOutputRuleNode(ruleNode)) { + outputLabels.add(ruleNode.getName()); + } + } + return outputLabels; + } + + @Override + public List getOutputLabelUsage(TenantId tenantId, RuleChainId ruleChainId) { + List ruleNodes = ruleChainService.findRuleNodesByTenantIdAndType(tenantId, TbRuleChainInputNode.class.getName(), ruleChainId.getId().toString()); + Map ruleChainNamesCache = new HashMap<>(); + // Additional filter, "just in case" the structure of the JSON configuration will change. + var filteredRuleNodes = ruleNodes.stream().filter(node -> { + try { + TbRuleChainInputNodeConfiguration configuration = JacksonUtil.treeToValue(node.getConfiguration(), TbRuleChainInputNodeConfiguration.class); + return ruleChainId.getId().toString().equals(configuration.getRuleChainId()); + } catch (Exception e) { + log.warn("[{}][{}] Failed to decode rule node configuration", tenantId, ruleChainId, e); + return false; + } + }).collect(Collectors.toList()); + + + return filteredRuleNodes.stream() + .map(ruleNode -> { + RuleChainOutputLabelsUsage usage = new RuleChainOutputLabelsUsage(); + usage.setRuleNodeId(ruleNode.getId()); + usage.setRuleNodeName(ruleNode.getName()); + usage.setRuleChainId(ruleNode.getRuleChainId()); + List relations = ruleChainService.getRuleNodeRelations(tenantId, ruleNode.getId()); + if (relations != null && !relations.isEmpty()) { + usage.setLabels(relations.stream().map(EntityRelation::getType).collect(Collectors.toSet())); + } + return usage; + }) + .filter(usage -> usage.getLabels() != null) + .peek(usage -> { + String ruleChainName = ruleChainNamesCache.computeIfAbsent(usage.getRuleChainId(), + id -> ruleChainService.findRuleChainById(tenantId, id).getName()); + usage.setRuleChainName(ruleChainName); + }) + .sorted(Comparator + .comparing(RuleChainOutputLabelsUsage::getRuleChainName) + .thenComparing(RuleChainOutputLabelsUsage::getRuleNodeName)) + .collect(Collectors.toList()); + } + + @Override + public List updateRelatedRuleChains(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateResult result) { + Set ruleChainIds = new HashSet<>(); + log.debug("[{}][{}] Going to update links in related rule chains", tenantId, ruleChainId); + if (result.getUpdatedRuleNodes() == null || result.getUpdatedRuleNodes().isEmpty()) { + return Collections.emptyList(); + } + + Set oldLabels = new HashSet<>(); + Set newLabels = new HashSet<>(); + Set confusedLabels = new HashSet<>(); + Map updatedLabels = new HashMap<>(); + for (RuleNodeUpdateResult update : result.getUpdatedRuleNodes()) { + var oldNode = update.getOldRuleNode(); + var newNode = update.getNewRuleNode(); + if (isOutputRuleNode(newNode)) { + try { + oldLabels.add(oldNode.getName()); + newLabels.add(newNode.getName()); + if (!oldNode.getName().equals(newNode.getName())) { + String oldLabel = oldNode.getName(); + String newLabel = newNode.getName(); + if (updatedLabels.containsKey(oldLabel) && !updatedLabels.get(oldLabel).equals(newLabel)) { + confusedLabels.add(oldLabel); + log.warn("[{}][{}] Can't automatically rename the label from [{}] to [{}] due to conflict [{}]", tenantId, ruleChainId, oldLabel, newLabel, updatedLabels.get(oldLabel)); + } else { + updatedLabels.put(oldLabel, newLabel); + } + + } + } catch (Exception e) { + log.warn("[{}][{}][{}] Failed to decode rule node configuration", tenantId, ruleChainId, newNode.getId(), e); + } + } + } + // Remove all output labels that are renamed to two or more different labels, since we don't which new label to use; + confusedLabels.forEach(updatedLabels::remove); + // Remove all output labels that are renamed but still present in the rule chain; + newLabels.forEach(updatedLabels::remove); + if (!oldLabels.equals(newLabels)) { + ruleChainIds.addAll(updateRelatedRuleChains(tenantId, ruleChainId, updatedLabels)); + } + return ruleChainIds.stream().map(id -> ruleChainService.findRuleChainById(tenantId, id)).collect(Collectors.toList()); + } + + public Set updateRelatedRuleChains(TenantId tenantId, RuleChainId ruleChainId, Map labelsMap) { + Set updatedRuleChains = new HashSet<>(); + List usageList = getOutputLabelUsage(tenantId, ruleChainId); + for (RuleChainOutputLabelsUsage usage : usageList) { + labelsMap.forEach((oldLabel, newLabel) -> { + if (usage.getLabels().contains(oldLabel)) { + updatedRuleChains.add(usage.getRuleChainId()); + renameOutgoingLinks(tenantId, usage.getRuleNodeId(), oldLabel, newLabel); + } + }); + } + return updatedRuleChains; + } + + private void renameOutgoingLinks(TenantId tenantId, RuleNodeId ruleNodeId, String oldLabel, String newLabel) { + List relations = ruleChainService.getRuleNodeRelations(tenantId, ruleNodeId); + for (EntityRelation relation : relations) { + if (relation.getType().equals(oldLabel)) { + relationService.deleteRelation(tenantId, relation); + relation.setType(newLabel); + relationService.saveRelation(tenantId, relation); + } + } + } + + private boolean isOutputRuleNode(RuleNode ruleNode) { + return isRuleNode(ruleNode, TbRuleChainOutputNode.class); + } + + private boolean isInputRuleNode(RuleNode ruleNode) { + return isRuleNode(ruleNode, TbRuleChainInputNode.class); + } + + private boolean isRuleNode(RuleNode ruleNode, Class clazz) { + return ruleNode != null && ruleNode.getType().equals(clazz.getName()); + } +} diff --git a/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java b/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java new file mode 100644 index 0000000000..ff2d738f9c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java @@ -0,0 +1,34 @@ +/** + * 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.rule; + +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; +import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; + +import java.util.List; +import java.util.Set; + +public interface TbRuleChainService { + + Set getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId); + + List getOutputLabelUsage(TenantId tenantId, RuleChainId ruleChainId); + + List updateRelatedRuleChains(TenantId tenantId, RuleChainId ruleChainId, RuleChainUpdateResult result); +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java index 7c9dd55c87..1f1200752f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java @@ -30,7 +30,10 @@ public class JwtHeaderTokenExtractor implements TokenExtractor { public String extract(HttpServletRequest request) { String header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM); if (StringUtils.isBlank(header)) { - throw new AuthenticationServiceException("Authorization header cannot be blank!"); + header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM_V2); + if (StringUtils.isBlank(header)) { + throw new AuthenticationServiceException("Authorization header cannot be blank!"); + } } if (header.length() < HEADER_PREFIX.length()) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java index a90effdd36..f645b989d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginRequest.java @@ -17,9 +17,14 @@ package org.thingsboard.server.service.security.auth.rest; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +@ApiModel public class LoginRequest { + private String username; + private String password; @JsonCreator @@ -28,10 +33,12 @@ public class LoginRequest { this.password = password; } + @ApiModelProperty(position = 1, required = true, value = "User email", example = "tenant@thingsboard.org") public String getUsername() { return username; } + @ApiModelProperty(position = 2, required = true, value = "User password", example = "tenant") public String getPassword() { return password; } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java new file mode 100644 index 0000000000..e39e0af14f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/LoginResponse.java @@ -0,0 +1,34 @@ +/** + * 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.auth.rest; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@ApiModel +@Data +public class LoginResponse { + + @ApiModelProperty(position = 1, required = true, value = "JWT token", + example = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZW5hbnRAdGhpbmdzYm9hcmQub3JnIi...") + private String token; + + @ApiModelProperty(position = 2, required = true, value = "Refresh token", + example = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZW5hbnRAdGhpbmdzYm9hcmQub3JnIi...") + private String refreshToken; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index c2890836b4..e701810a6c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -58,7 +58,7 @@ public enum Resource { public static Resource of(EntityType entityType) { for (Resource resource : Resource.values()) { - if (resource.getEntityType().get() == entityType) { + if (resource.getEntityType().orElse(null) == entityType) { return resource; } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java index 15cf6f1a17..6e5ca1669f 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java @@ -222,7 +222,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } return subscriptionUpdate; - }); + }, true); if (entityId.getEntityType() == EntityType.DEVICE) { updateDeviceInactivityTimeout(tenantId, entityId, ts); } @@ -256,7 +256,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } return subscriptionUpdate; - }); + }, true); if (entityId.getEntityType() == EntityType.DEVICE) { if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { updateDeviceInactivityTimeout(tenantId, entityId, attributes); @@ -333,14 +333,15 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } } return subscriptionUpdate; - }); + }, false); callback.onSuccess(); } private void onLocalTelemetrySubUpdate(EntityId entityId, Function castFunction, Predicate filterFunction, - Function> processFunction) { + Function> processFunction, + boolean ignoreEmptyUpdates) { Set entitySubscriptions = subscriptionsByEntityId.get(entityId); if (entitySubscriptions != null) { entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { @@ -351,7 +352,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); } else { TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); - toCoreNotificationsProducer.send(tpi, toProto(s, subscriptionUpdate), null); + toCoreNotificationsProducer.send(tpi, toProto(s, subscriptionUpdate, ignoreEmptyUpdates), null); } } }); @@ -467,6 +468,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } private TbProtoQueueMsg toProto(TbSubscription subscription, List updates) { + return toProto(subscription, updates, true); + } + + private TbProtoQueueMsg toProto(TbSubscription subscription, List updates, boolean ignoreEmptyUpdates) { TbSubscriptionUpdateProto.Builder builder = TbSubscriptionUpdateProto.newBuilder(); builder.setSessionId(subscription.getSessionId()); @@ -487,14 +492,16 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene boolean hasData = false; for (Object v : value) { Object[] array = (Object[]) v; - dataBuilder.addTs((long) array[0]); + TbSubscriptionUpdateTsValue.Builder tsValueBuilder = TbSubscriptionUpdateTsValue.newBuilder(); + tsValueBuilder.setTs((long) array[0]); String strVal = (String) array[1]; if (strVal != null) { hasData = true; - dataBuilder.addValue(strVal); + tsValueBuilder.setValue(strVal); } + dataBuilder.addTsValue(tsValueBuilder.build()); } - if (hasData) { + if (!ignoreEmptyUpdates || hasData) { builder.addData(dataBuilder.build()); } }); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 978d7aaac3..25b67d3a06 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -42,6 +42,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseP import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionKetStateProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateProto; +import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateTsValue; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscriptionProto; import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -171,10 +172,11 @@ public class TbSubscriptionUtils { Map> data = new TreeMap<>(); proto.getDataList().forEach(v -> { List values = data.computeIfAbsent(v.getKey(), k -> new ArrayList<>()); - for (int i = 0; i < v.getTsCount(); i++) { + for (int i = 0; i < v.getTsValueCount(); i++) { Object[] value = new Object[2]; - value[0] = v.getTs(i); - value[1] = v.getValue(i); + TbSubscriptionUpdateTsValue tsValue = v.getTsValue(i); + value[0] = tsValue.getTs(); + value[1] = tsValue.hasValue() ? tsValue.getValue() : null; values.add(value); } }); 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/BasicCredentialsValidationResult.java b/application/src/main/java/org/thingsboard/server/service/transport/BasicCredentialsValidationResult.java new file mode 100644 index 0000000000..56d7b776e8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/transport/BasicCredentialsValidationResult.java @@ -0,0 +1,18 @@ +/** + * 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.transport; + +enum BasicCredentialsValidationResult {HASH_MISMATCH, PASSWORD_MISMATCH, VALID} diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index dc5f6c436d..1cefefd5d1 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -25,7 +25,6 @@ import com.google.protobuf.ByteString; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cache.ota.OtaPackageDataCache; import org.thingsboard.server.common.data.ApiUsageState; @@ -37,6 +36,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; @@ -107,6 +107,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.PASSWORD_MISMATCH; +import static org.thingsboard.server.service.transport.BasicCredentialsValidationResult.VALID; + /** * Created by ashvayka on 05.10.18. */ @@ -181,71 +184,89 @@ public class DefaultTransportApiService implements TransportApiService { //TODO: Make async and enable caching DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId); if (credentials != null && credentials.getCredentialsType() == credentialsType) { - return getDeviceInfo(credentials.getDeviceId(), credentials); + return getDeviceInfo(credentials); } else { return getEmptyTransportApiResponseFuture(); } } private ListenableFuture validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { - DeviceCredentials credentials = null; - if (!StringUtils.isEmpty(mqtt.getUserName())) { - credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); + DeviceCredentials credentials; + if (StringUtils.isEmpty(mqtt.getUserName())) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId())); if (credentials != null) { - if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) { - return getDeviceInfo(credentials.getDeviceId(), credentials); - } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { - if (!checkMqttCredentials(mqtt, credentials)) { - credentials = null; - } - } else { + return getDeviceInfo(credentials); + } else { + return getEmptyTransportApiResponseFuture(); + } + } else { + credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId( + EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName())); + if (checkIsMqttCredentials(credentials)) { + var validationResult = validateMqttCredentials(mqtt, credentials); + if (VALID.equals(validationResult)) { + return getDeviceInfo(credentials); + } else if (PASSWORD_MISMATCH.equals(validationResult)) { return getEmptyTransportApiResponseFuture(); + } else { + return validateUserNameCredentials(mqtt); } + } else { + return validateUserNameCredentials(mqtt); } - if (credentials == null) { - credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName())); - } - } - if (credentials == null) { - credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId())); } + } + + private ListenableFuture validateUserNameCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); if (credentials != null) { - return getDeviceInfo(credentials.getDeviceId(), credentials); - } else { - return getEmptyTransportApiResponseFuture(); + switch (credentials.getCredentialsType()) { + case ACCESS_TOKEN: + return getDeviceInfo(credentials); + case MQTT_BASIC: + if (VALID.equals(validateMqttCredentials(mqtt, credentials))) { + return getDeviceInfo(credentials); + } else { + return getEmptyTransportApiResponseFuture(); + } + } } + return getEmptyTransportApiResponseFuture(); + } + + private static boolean checkIsMqttCredentials(DeviceCredentials credentials) { + return credentials != null && DeviceCredentialsType.MQTT_BASIC.equals(credentials.getCredentialsType()); } private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, String credId) { - DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId); + return checkMqttCredentials(clientCred, deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId)); + } + + private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) { if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { - if (!checkMqttCredentials(clientCred, deviceCredentials)) { - return null; - } else { + if (VALID.equals(validateMqttCredentials(clientCred, deviceCredentials))) { return deviceCredentials; } } return null; } - private boolean checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) { + private BasicCredentialsValidationResult validateMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) { BasicMqttCredentials dbCred = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); if (!StringUtils.isEmpty(dbCred.getClientId()) && !dbCred.getClientId().equals(clientCred.getClientId())) { - return false; + return BasicCredentialsValidationResult.HASH_MISMATCH; } if (!StringUtils.isEmpty(dbCred.getUserName()) && !dbCred.getUserName().equals(clientCred.getUserName())) { - return false; + return BasicCredentialsValidationResult.HASH_MISMATCH; } if (!StringUtils.isEmpty(dbCred.getPassword())) { if (StringUtils.isEmpty(clientCred.getPassword())) { - return false; + return BasicCredentialsValidationResult.PASSWORD_MISMATCH; } else { - if (!dbCred.getPassword().equals(clientCred.getPassword())) { - return false; - } + return dbCred.getPassword().equals(clientCred.getPassword()) ? VALID : BasicCredentialsValidationResult.PASSWORD_MISMATCH; } } - return true; + return VALID; } private ListenableFuture handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { @@ -437,10 +458,10 @@ public class DefaultTransportApiService implements TransportApiService { .build()); } - private ListenableFuture getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) { - return Futures.transform(deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, deviceId), device -> { + private ListenableFuture getDeviceInfo(DeviceCredentials credentials) { + return Futures.transform(deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, credentials.getDeviceId()), device -> { if (device == null) { - log.trace("[{}] Failed to lookup device by id", deviceId); + log.trace("[{}] Failed to lookup device by id", credentials.getDeviceId()); return getEmptyTransportApiResponse(); } try { @@ -458,7 +479,7 @@ public class DefaultTransportApiService implements TransportApiService { return TransportApiResponseMsg.newBuilder() .setValidateCredResponseMsg(builder.build()).build(); } catch (JsonProcessingException e) { - log.warn("[{}] Failed to lookup device by id", deviceId, e); + log.warn("[{}] Failed to lookup device by id", credentials.getDeviceId(), e); return getEmptyTransportApiResponse(); } }, MoreExecutors.directExecutor()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2c8d0c63bc..8f02a7cb02 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -23,18 +23,38 @@ server: ssl: # Enable/disable SSL support enabled: "${SSL_ENABLED:false}" - # Path to the key store that holds the SSL certificate - key-store: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}" - # Password used to access the key store - key-store-password: "${SSL_KEY_STORE_PASSWORD:thingsboard}" - # Type of the key store - key-store-type: "${SSL_KEY_STORE_TYPE:PKCS12}" - # Alias that identifies the key in the key store - key-alias: "${SSL_KEY_ALIAS:tomcat}" + # Server SSL credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${SSL_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${SSL_PEM_CERT:server.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${SSL_PEM_KEY:server_key.pem}" + # Server certificate private key password (optional) + key_password: "${SSL_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${SSL_KEY_STORE_TYPE:PKCS12}" + # Path to the key store that holds the SSL certificate + store_file: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}" + # Password used to access the key store + store_password: "${SSL_KEY_STORE_PASSWORD:thingsboard}" + # Key alias + key_alias: "${SSL_KEY_ALIAS:tomcat}" + # Password used to access the key + key_password: "${SSL_KEY_PASSWORD:thingsboard}" + # HTTP/2 support (takes effect only if server SSL is enabled) + http2: + # Enable/disable HTTP/2 support + enabled: "${HTTP2_ENABLED:true}" log_controller_error_stack_trace: "${HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE:false}" ws: send_timeout: "${TB_SERVER_WS_SEND_TIMEOUT:5000}" - ping_timeout: "${TB_SERVER_WS_PING_TIMEOUT:30000}" + ping_timeout: "${TB_SERVER_WS_PING_TIMEOUT:15000}" limits: # Limit the amount of sessions and subscriptions available on each server. Put values to zero to disable particular limitation max_sessions_per_tenant: "${TB_SERVER_WS_TENANT_RATE_LIMITS_MAX_SESSIONS_PER_TENANT:0}" @@ -69,6 +89,11 @@ server: # Default value of the server side RPC timeout. default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}" +# Application info +app: + # Application version + version: "@project.version@" + # Zookeeper connection parameters. Used for service discovery. zk: # Enable/disable zookeeper discovery service. @@ -129,10 +154,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 @@ -180,7 +211,7 @@ cassandra: init_retry_interval_ms: "${CASSANDRA_CLUSTER_INIT_RETRY_INTERVAL_MS:3000}" max_requests_per_connection_local: "${CASSANDRA_MAX_REQUESTS_PER_CONNECTION_LOCAL:32768}" max_requests_per_connection_remote: "${CASSANDRA_MAX_REQUESTS_PER_CONNECTION_REMOTE:32768}" - # Credential parameters # + # Credential parameters credentials: "${CASSANDRA_USE_CREDENTIALS:false}" # Specify your username username: "${CASSANDRA_USERNAME:}" @@ -279,6 +310,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: @@ -478,6 +511,8 @@ spring: repositories: enabled: "true" jpa: + properties: + javax.persistence.query.timeout: "${JAVAX_PERSISTENCE_QUERY_TIMEOUT:30000}" open-in-view: "false" hibernate: ddl-auto: "none" @@ -560,7 +595,7 @@ js: remote: # Maximum allowed JavaScript execution errors before JavaScript will be blacklisted max_errors: "${REMOTE_JS_SANDBOX_MAX_ERRORS:3}" - # Maximum time in seconds for black listed function to stay in 1:the list. + # Maximum time in seconds for black listed function to stay in the list. max_black_list_duration_sec: "${REMOTE_JS_SANDBOX_MAX_BLACKLIST_DURATION_SEC:60}" stats: enabled: "${TB_JS_REMOTE_STATS_ENABLED:false}" @@ -609,16 +644,32 @@ transport: bind_address: "${MQTT_SSL_BIND_ADDRESS:0.0.0.0}" # MQTT SSL bind port bind_port: "${MQTT_SSL_BIND_PORT:8883}" - # SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext + # SSL protocol: See https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}" - # Path to the key store that holds the SSL certificate - key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}" - # Password used to access the key store - key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}" - # Password used to access the key - key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" - # Type of the key store - key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}" + # Server SSL credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${MQTT_SSL_PEM_CERT:mqttserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${MQTT_SSL_PEM_KEY:mqttserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${MQTT_SSL_KEY_STORE:mqttserver.jks}" + # Password used to access the key store + store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}" + # Optional alias of the private key; If not set, the platform will load the first private key from the keystore; + key_alias: "${MQTT_SSL_KEY_ALIAS:}" + # Optional password to access the private key. If not set, the platform will attempt to load the private keys that are not protected with the password; + key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" # Skip certificate validity check for client certificates. skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" # Local CoAP transport parameters @@ -637,14 +688,30 @@ transport: bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" - # Path to the key store that holds the certificate - key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" - # Password used to access the key store - key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" - # Password used to access the key - key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" - # Key alias - key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Server DTLS credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" x509: # Skip certificate validity check for client certificates. skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" @@ -661,9 +728,33 @@ transport: security: bind_address: "${LWM2M_SECURITY_BIND_ADDRESS:0.0.0.0}" bind_port: "${LWM2M_SECURITY_BIND_PORT:5686}" + # Server X509 Certificates support + credentials: + # Whether to enable LWM2M server X509 Certificate/RPK support + enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:true}" + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${LWM2M_SERVER_PEM_CERT:lwm2mserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${LWM2M_SERVER_PEM_KEY:lwm2mserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${LWM2M_SERVER_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${LWM2M_SERVER_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${LWM2M_SERVER_KEY_STORE:lwm2mserver.jks}" + # Password used to access the key store + store_password: "${LWM2M_SERVER_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}" + # Password used to access the key + key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_ks_password}" # Only Certificate_x509: - key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}" - key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_ks_password}" skip_validity_check_for_client_cert: "${TB_LWM2M_SERVER_SECURITY_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" bootstrap: enable: "${LWM2M_ENABLED_BS:true}" @@ -673,18 +764,51 @@ transport: security: bind_address: "${LWM2M_BS_SECURITY_BIND_ADDRESS:0.0.0.0}" bind_port: "${LWM2M_BS_SECURITY_BIND_PORT:5688}" - # Only Certificate_x509: - key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}" - key_password: "${LWM2M_BS_KEY_PASSWORD:server_ks_password}" + # Bootstrap server X509 Certificates support + credentials: + # Whether to enable LWM2M bootstrap server X509 Certificate/RPK support + enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:true}" + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${LWM2M_BS_PEM_CERT:lwm2mserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${LWM2M_BS_PEM_KEY:lwm2mserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${LWM2M_BS_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${LWM2M_BS_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${LWM2M_BS_KEY_STORE:lwm2mserver.jks}" + # Password used to access the key store + store_password: "${LWM2M_BS_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}" + # Password used to access the key + key_password: "${LWM2M_BS_KEY_PASSWORD:server_ks_password}" security: - # Certificate_x509: - # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format - # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh - key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks" - key_store: "${LWM2M_KEYSTORE:lwm2mserver.jks}" - key_store_password: "${LWM2M_KEYSTORE_PASSWORD:server_ks_password}" - root_alias: "${LWM2M_SERVER_ROOT_CA_ALIAS:rootca}" + # X509 trust certificates + trust-credentials: + # Whether to load X509 trust certificates + enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:true}" + # Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore) + type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}" + # PEM certificates + pem: + # Path to the certificates file (holds trust certificates) + cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}" + # Keystore with trust certificates + keystore: + # Type of the key store + type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the X509 certificates + store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}" + # Password used to access the key store + store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" timeout: "${LWM2M_TIMEOUT:120000}" @@ -692,9 +816,11 @@ transport: downlink_pool_size: "${LWM2M_DOWNLINK_POOL_SIZE:10}" ota_pool_size: "${LWM2M_OTA_POOL_SIZE:10}" clean_period_in_sec: "${LWM2M_CLEAN_PERIOD_IN_SEC:2}" - log_max_length: "${LWM2M_LOG_MAX_LENGTH:1024}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" + network_config: # In this section you can specify custom parameters for LwM2M network configuration and expose the env variables to configure outside +# - key: "PROTOCOL_STAGE_THREAD_COUNT" +# value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}" snmp: enabled: "${SNMP_ENABLED:true}" response_processing: @@ -730,19 +856,19 @@ edges: persistToTelemetry: "${EDGES_PERSIST_STATE_TO_TELEMETRY:false}" swagger: - api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}" - security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api.*}" - non_security_path_regex: "${SWAGGER_NON_SECURITY_PATH_REGEX:/api/noauth.*}" + api_path_regex: "${SWAGGER_API_PATH_REGEX:/api/.*}" + security_path_regex: "${SWAGGER_SECURITY_PATH_REGEX:/api/.*}" + non_security_path_regex: "${SWAGGER_NON_SECURITY_PATH_REGEX:/api/(?:noauth|v1)/.*}" title: "${SWAGGER_TITLE:ThingsBoard REST API}" - description: "${SWAGGER_DESCRIPTION:For instructions how to authorize requests please visit REST API documentation page.}" + description: "${SWAGGER_DESCRIPTION: ThingsBoard open-source IoT platform REST API documentation.}" contact: - name: "${SWAGGER_CONTACT_NAME:Thingsboard team}" - url: "${SWAGGER_CONTACT_URL:http://thingsboard.io}" + name: "${SWAGGER_CONTACT_NAME:ThingsBoard team}" + url: "${SWAGGER_CONTACT_URL:https://thingsboard.io}" email: "${SWAGGER_CONTACT_EMAIL:info@thingsboard.io}" license: title: "${SWAGGER_LICENSE_TITLE:Apache License Version 2.0}" url: "${SWAGGER_LICENSE_URL:https://github.com/thingsboard/thingsboard/blob/master/LICENSE}" - version: "${SWAGGER_VERSION:2.0}" + version: "${SWAGGER_VERSION:}" queue: type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) @@ -771,10 +897,15 @@ queue: sasl.mechanism: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_MECHANISM:PLAIN}" sasl.config: "${TB_QUEUE_KAFKA_CONFLUENT_SASL_JAAS_CONFIG:org.apache.kafka.common.security.plain.PlainLoginModule required username=\"CLUSTER_API_KEY\" password=\"CLUSTER_API_SECRET\";}" security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}" + # Key-value properties for Kafka consumer per specific topic, e.g. tb_ota_package is a topic name for ota, tb_rule_engine.sq is a topic name for default SequentialByOriginator queue. + # Check TB_QUEUE_CORE_OTA_TOPIC and TB_QUEUE_RE_SQ_TOPIC params consumer-properties-per-topic: tb_ota_package: - key: max.poll.records - value: 10 + value: "${TB_QUEUE_KAFKA_OTA_MAX_POLL_RECORDS:10}" +# tb_rule_engine.sq: +# - key: max.poll.records +# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) @@ -901,8 +1032,8 @@ queue: # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries; - max-pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:3}"# Max allowed time in seconds for pause between retries. + pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}" # Time in seconds to wait in consumer thread before retries; + max-pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:3}" # Max allowed time in seconds for pause between retries. - name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}" topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}" poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}" @@ -918,8 +1049,8 @@ queue: # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - max-pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}"# Max allowed time in seconds for pause between retries. + pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}" # Time in seconds to wait in consumer thread before retries; + max-pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}" # Max allowed time in seconds for pause between retries. - name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}" topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}" poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}" @@ -935,8 +1066,8 @@ queue: # For RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages; - pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries; - max-pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}"# Max allowed time in seconds for pause between retries. + pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}" # Time in seconds to wait in consumer thread before retries; + max-pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}" # Max allowed time in seconds for pause between retries. transport: # For high priority notifications that require minimum latency and processing time notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" diff --git a/application/src/test/java/org/thingsboard/server/actors/stats/StatsActorTest.java b/application/src/test/java/org/thingsboard/server/actors/stats/StatsActorTest.java new file mode 100644 index 0000000000..c058bb881e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/actors/stats/StatsActorTest.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.actors.stats; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.actors.ActorSystemContext; +import org.thingsboard.server.common.data.Event; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.event.EventService; +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +class StatsActorTest { + + StatsActor statsActor; + ActorSystemContext actorSystemContext; + EventService eventService; + TbServiceInfoProvider serviceInfoProvider; + + @BeforeEach + void setUp() { + actorSystemContext = mock(ActorSystemContext.class); + + eventService = mock(EventService.class); + willReturn(eventService).given(actorSystemContext).getEventService(); + serviceInfoProvider = mock(TbServiceInfoProvider.class); + willReturn(serviceInfoProvider).given(actorSystemContext).getServiceInfoProvider(); + + statsActor = new StatsActor(actorSystemContext); + } + + @Test + void givenEmptyStatMessage_whenOnStatsPersistMsg_thenNoAction() { + StatsPersistMsg emptyStats = new StatsPersistMsg(0, 0, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); + statsActor.onStatsPersistMsg(emptyStats); + verify(actorSystemContext, never()).getEventService(); + } + + @Test + void givenNonEmptyStatMessage_whenOnStatsPersistMsg_thenNoAction() { + statsActor.onStatsPersistMsg(new StatsPersistMsg(0, 1, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)); + verify(eventService, times(1)).save(any(Event.class)); + statsActor.onStatsPersistMsg(new StatsPersistMsg(1, 0, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)); + verify(eventService, times(2)).save(any(Event.class)); + statsActor.onStatsPersistMsg(new StatsPersistMsg(1, 1, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID)); + verify(eventService, times(3)).save(any(Event.class)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/actors/stats/StatsPersistMsgTest.java b/application/src/test/java/org/thingsboard/server/actors/stats/StatsPersistMsgTest.java new file mode 100644 index 0000000000..e4fcf7cfc4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/actors/stats/StatsPersistMsgTest.java @@ -0,0 +1,38 @@ +/** + * 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.actors.stats; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.id.TenantId; + +import static org.assertj.core.api.Assertions.assertThat; + +class StatsPersistMsgTest { + + @Test + void testIsEmpty() { + StatsPersistMsg emptyStats = new StatsPersistMsg(0, 0, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID); + assertThat(emptyStats.isEmpty()).isTrue(); + } + + @Test + void testNotEmpty() { + assertThat(new StatsPersistMsg(1, 0, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isEmpty()).isFalse(); + assertThat(new StatsPersistMsg(0, 1, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isEmpty()).isFalse(); + assertThat(new StatsPersistMsg(1, 1, TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID).isEmpty()).isFalse(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 244eb0e05d..c72b732ff6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -99,6 +99,20 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(foundAsset.getName(), savedAsset.getName()); } + @Test + public void testSaveAssetWithViolationOfLengthValidation() throws Exception { + Asset asset = new Asset(); + asset.setName(RandomStringUtils.randomAlphabetic(300)); + asset.setType("default"); + doPost("/api/asset", asset).andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + asset.setName("Normal name"); + asset.setType(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/asset", asset).andExpect(statusReason(containsString("length of type must be equal or less than 255"))); + asset.setType("default"); + asset.setLabel(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/asset", asset).andExpect(statusReason(containsString("length of label must be equal or less than 255"))); + } + @Test public void testUpdateAssetFromDifferentTenant() throws Exception { Asset asset = new Asset(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java index 4fbf08e595..318e1413a0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java @@ -90,6 +90,28 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest .andExpect(status().isOk()); } + @Test + public void testSaveCustomerWithViolationOfValidation() throws Exception { + Customer customer = new Customer(); + customer.setTitle(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/customer", customer).andExpect(statusReason(containsString("length of title must be equal or less than 255"))); + customer.setTitle("Normal title"); + customer.setCity(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/customer", customer).andExpect(statusReason(containsString("length of city must be equal or less than 255"))); + customer.setCity("Normal city"); + customer.setCountry(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/customer", customer).andExpect(statusReason(containsString("length of country must be equal or less than 255"))); + customer.setCountry("Ukraine"); + customer.setPhone(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/customer", customer).andExpect(statusReason(containsString("length of phone must be equal or less than 255"))); + customer.setPhone("+3892555554512"); + customer.setState(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/customer", customer).andExpect(statusReason(containsString("length of state must be equal or less than 255"))); + customer.setState("Normal state"); + customer.setZip(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/customer", customer).andExpect(statusReason(containsString("length of zip or postal code must be equal or less than 255"))); + } + @Test public void testUpdateCustomerFromDifferentTenant() throws Exception { Customer customer = new Customer(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index 60b4128878..18fa8bdd5d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -93,6 +93,13 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertEquals(foundDashboard.getTitle(), savedDashboard.getTitle()); } + @Test + public void testSaveDashboardInfoWithViolationOfValidation() throws Exception { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/dashboard", dashboard).andExpect(statusReason(containsString("length of title must be equal or less than 255"))); + } + @Test public void testUpdateDashboardFromDifferentTenant() throws Exception { Dashboard dashboard = new Dashboard(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index fa5a6b76eb..c31f5c7daf 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -114,6 +114,20 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { Assert.assertEquals(foundDevice.getName(), savedDevice.getName()); } + @Test + public void saveDeviceWithViolationOfValidation() throws Exception { + Device device = new Device(); + device.setName(RandomStringUtils.randomAlphabetic(300)); + device.setType("default"); + doPost("/api/device", device).andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + device.setName("Normal Name"); + device.setType(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/device", device).andExpect(statusReason(containsString("length of type must be equal or less than 255"))); + device.setType("Normal type"); + device.setLabel(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/device", device).andExpect(statusReason(containsString("length of label must be equal or less than 255"))); + } + @Test public void testUpdateDeviceFromDifferentTenant() throws Exception { Device device = new Device(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java index 3abd9f8e37..d33a93cd64 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -22,6 +22,7 @@ import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import com.squareup.wire.schema.internal.parser.ProtoFileElement; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -109,6 +110,12 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName()); } + @Test + public void saveDeviceProfileWithViolationOfValidation() throws Exception { + doPost("/api/deviceProfile", this.createDeviceProfile(RandomStringUtils.randomAlphabetic(300), null)) + .andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + } + @Test public void testFindDeviceProfileById() throws Exception { DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile", null); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index f919804ce9..9259f3253d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Random; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.nullValue; @@ -112,6 +113,18 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); } + @Test + public void testSaveEdgeWithViolationOfLengthValidation() throws Exception { + Edge edge = constructEdge(RandomStringUtils.randomAlphabetic(300), "default"); + doPost("/api/edge", edge).andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + edge.setName("normal name"); + edge.setType(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/edge", edge).andExpect(statusReason(containsString("length of type must be equal or less than 255"))); + edge.setType("normal type"); + edge.setLabel(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/edge", edge).andExpect(statusReason(containsString("length of label must be equal or less than 255"))); + } + @Test public void testFindEdgeById() throws Exception { Edge edge = constructEdge("My edge", "default"); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index e651caa2ff..304964dfa8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -133,6 +133,14 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes assertEquals(foundEntityView.getKeys(), telemetry); } + @Test + public void testSaveEntityViewWithViolationOfValidation() throws Exception { + EntityView entityView = createEntityView(RandomStringUtils.randomAlphabetic(300), 0, 0); + doPost("/api/entityView", entityView).andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + entityView.setName("Normal name"); + entityView.setType(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/entityView", entityView).andExpect(statusReason(containsString("length of type must be equal or less than 255"))); + } @Test public void testUpdateEntityViewFromDifferentTenant() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java index 7026548b25..065fc4bab3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -40,6 +41,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; @@ -117,6 +119,24 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes Assert.assertEquals(foundFirmwareInfo.getTitle(), savedFirmwareInfo.getTitle()); } + @Test + public void saveOtaPackageInfoWithViolationOfLengthValidation() throws Exception { + SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); + firmwareInfo.setDeviceProfileId(deviceProfileId); + firmwareInfo.setType(FIRMWARE); + firmwareInfo.setTitle(RandomStringUtils.randomAlphabetic(300)); + firmwareInfo.setVersion(VERSION); + firmwareInfo.setUsesUrl(false); + doPost("/api/otaPackage", firmwareInfo).andExpect(statusReason(containsString("length of title must be equal or less than 255"))); + firmwareInfo.setTitle(TITLE); + firmwareInfo.setVersion(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/otaPackage", firmwareInfo).andExpect(statusReason(containsString("length of version must be equal or less than 255"))); + firmwareInfo.setVersion(VERSION); + firmwareInfo.setUsesUrl(true); + firmwareInfo.setUrl(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/otaPackage", firmwareInfo).andExpect(statusReason(containsString("length of url must be equal or less than 255"))); + } + @Test public void testSaveFirmwareData() throws Exception { SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java index 67addcd947..6363ca3a67 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -33,6 +34,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class BaseRuleChainControllerTest extends AbstractControllerTest { @@ -84,6 +86,13 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest Assert.assertEquals(savedRuleChain.getName(), foundRuleChain.getName()); } + @Test + public void testSaveRuleChainWithViolationOfLengthValidation() throws Exception { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/ruleChain", ruleChain).andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + } + @Test public void testFindRuleChainById() throws Exception { RuleChain ruleChain = new RuleChain(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java index 8850b70f86..11a8ff7718 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -33,6 +34,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public abstract class BaseTbResourceControllerTest extends AbstractControllerTest { @@ -98,6 +100,16 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes Assert.assertEquals(foundResource.getTitle(), savedResource.getTitle()); } + @Test + public void saveResourceInfoWithViolationOfLengthValidation() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle(RandomStringUtils.randomAlphabetic(300)); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setData("Test Data"); + doPost("/api/resource", resource).andExpect(statusReason(containsString("length of title must be equal or less than 255"))); + } + @Test public void testUpdateTbResourceFromDifferentTenant() throws Exception { TbResource resource = new TbResource(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java index b05dd52bb5..51a7d81df8 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java @@ -52,6 +52,14 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { doDelete("/api/tenant/"+savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } + + @Test + public void testSaveTenantWithViolationOfValidation() throws Exception { + loginSysAdmin(); + Tenant tenant = new Tenant(); + tenant.setTitle(RandomStringUtils.randomAlphanumeric(300)); + doPost("/api/tenant", tenant).andExpect(statusReason(containsString("length of title must be equal or less than 255"))); + } @Test public void testFindTenantById() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java index bf8ab3e343..c33b8a3f29 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Test; @@ -67,6 +68,13 @@ public abstract class BaseTenantProfileControllerTest extends AbstractController Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName()); } + @Test + public void testSaveTenantProfileWithViolationOfLengthValidation() throws Exception { + loginSysAdmin(); + TenantProfile tenantProfile = this.createTenantProfile(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/tenantProfile", tenantProfile).andExpect(statusReason(containsString("length of name must be equal or less than 255"))); + } + @Test public void testFindTenantProfileById() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 600b9656f2..e25b5e35ad 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -105,6 +105,28 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testSaveUserWithViolationOfFiledValidation() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + String email = "tenant2@thingsboard.org"; + User user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(savedTenant.getId()); + user.setEmail(email); + user.setFirstName(RandomStringUtils.randomAlphabetic(300)); + user.setLastName("Downs"); + doPost("/api/user", user).andExpect(statusReason(containsString("Validation error: length of first name must be equal or less than 255"))); + user.setFirstName("Normal name"); + user.setLastName(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/user", user).andExpect(statusReason(containsString("length of last name must be equal or less than 255"))); + } + @Test public void testUpdateUserFromDifferentTenant() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java index 4b16cee03d..3167466d93 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -88,6 +89,13 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController Assert.assertEquals(foundWidgetsBundle.getTitle(), savedWidgetsBundle.getTitle()); } + @Test + public void testSaveWidgetBundleWithViolationOfLengthValidation() throws Exception { + WidgetsBundle widgetsBundle = new WidgetsBundle(); + widgetsBundle.setTitle(RandomStringUtils.randomAlphabetic(300)); + doPost("/api/widgetsBundle", widgetsBundle).andExpect(statusReason(containsString("length of title must be equal or less than 255"))); + } + @Test public void testUpdateWidgetsBundleFromDifferentTenant() throws Exception { WidgetsBundle widgetsBundle = new WidgetsBundle(); diff --git a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java new file mode 100644 index 0000000000..2a37fa7c2a --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java @@ -0,0 +1,97 @@ +/** + * 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.install.update; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willCallRealMethod; + +@ActiveProfiles("install") +@SpringBootTest(classes = DefaultDataUpdateService.class) +class DefaultDataUpdateServiceTest { + + ObjectMapper mapper = new ObjectMapper(); + + @MockBean + DefaultDataUpdateService service; + + @BeforeEach + void setUp() { + willCallRealMethod().given(service).convertDeviceProfileAlarmRulesForVersion330(any()); + willCallRealMethod().given(service).convertDeviceProfileForVersion330(any()); + } + + JsonNode readFromResource(String resourceName) throws IOException { + return mapper.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName)); + } + + @Test + void convertDeviceProfileAlarmRulesForVersion330FirstRun() throws IOException { + JsonNode spec = readFromResource("update/330/device_profile_001_in.json"); + JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); + + assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isTrue(); + assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature + } + + @Test + void convertDeviceProfileAlarmRulesForVersion330SecondRun() throws IOException { + JsonNode spec = readFromResource("update/330/device_profile_001_out.json"); + JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); + + assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isFalse(); + assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature + } + + @Test + void convertDeviceProfileAlarmRulesForVersion330EmptyJson() throws JsonProcessingException { + JsonNode spec = mapper.readTree("{ }"); + JsonNode expected = mapper.readTree("{ }"); + + assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); + assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); + } + + @Test + void convertDeviceProfileAlarmRulesForVersion330AlarmNodeNull() throws JsonProcessingException { + JsonNode spec = mapper.readTree("{ \"alarms\" : null }"); + JsonNode expected = mapper.readTree("{ \"alarms\" : null }"); + + assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); + assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); + } + + @Test + void convertDeviceProfileAlarmRulesForVersion330NoAlarmNode() throws JsonProcessingException { + JsonNode spec = mapper.readTree("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); + JsonNode expected = mapper.readTree("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); + + assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); + assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java index 7454589a6d..8de16413c3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/transport/TransportSqlTestSuite.java @@ -29,6 +29,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage; "org.thingsboard.server.transport.*.attributes.request.sql.*Test", "org.thingsboard.server.transport.*.claim.sql.*Test", "org.thingsboard.server.transport.*.provision.sql.*Test", + "org.thingsboard.server.transport.*.credentials.sql.*Test", "org.thingsboard.server.transport.lwm2m.sql.*Test" }) public class TransportSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 0d11c9a773..d815e85990 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -61,8 +61,12 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg protected DeviceProfile deviceProfile; - protected void processBeforeTest (String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { - this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { + this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); + } + + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic, boolean enableCompatibilityWithJsonPayloadFormat, boolean useJsonPayloadFormatForDefaultDownlinkTopics) throws Exception { + this.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED, enableCompatibilityWithJsonPayloadFormat, useJsonPayloadFormatForDefaultDownlinkTopics); } protected void processBeforeTest(String deviceName, @@ -76,8 +80,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg String rpcRequestProtoSchema, String provisionKey, String provisionSecret, - DeviceProfileProvisionType provisionType - ) throws Exception { + DeviceProfileProvisionType provisionType, + boolean enableCompatibilityWithJsonPayloadFormat, + boolean useJsonPayloadFormatForDefaultDownlinkTopics) throws Exception { loginSysAdmin(); Tenant tenant = new Tenant(); @@ -106,7 +111,7 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg gateway.setAdditionalInfo(additionalInfo); if (payloadType != null) { - DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, telemetryProtoSchema, attributesProtoSchema, rpcResponseProtoSchema, rpcRequestProtoSchema, provisionKey, provisionSecret, provisionType); + DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic, telemetryProtoSchema, attributesProtoSchema, rpcResponseProtoSchema, rpcRequestProtoSchema, provisionKey, provisionSecret, provisionType, enableCompatibilityWithJsonPayloadFormat, useJsonPayloadFormatForDefaultDownlinkTopics); deviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class); device.setType(deviceProfile.getName()); device.setDeviceProfileId(deviceProfile.getId()); @@ -162,7 +167,9 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg String telemetryProtoSchema, String attributesProtoSchema, String rpcResponseProtoSchema, String rpcRequestProtoSchema, String provisionKey, String provisionSecret, - DeviceProfileProvisionType provisionType) { + DeviceProfileProvisionType provisionType, + boolean enableCompatibilityWithJsonPayloadFormat, + boolean useJsonPayloadFormatForDefaultDownlinkTopics) { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setName(transportPayloadType.name()); deviceProfile.setType(DeviceProfileType.DEFAULT); @@ -200,6 +207,8 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg protoTransportPayloadConfiguration.setDeviceAttributesProtoSchema(attributesProtoSchema); protoTransportPayloadConfiguration.setDeviceRpcResponseProtoSchema(rpcResponseProtoSchema); protoTransportPayloadConfiguration.setDeviceRpcRequestProtoSchema(rpcRequestProtoSchema); + protoTransportPayloadConfiguration.setEnableCompatibilityWithJsonPayloadFormat(enableCompatibilityWithJsonPayloadFormat); + protoTransportPayloadConfiguration.setUseJsonPayloadFormatForDefaultDownlinkTopics(enableCompatibilityWithJsonPayloadFormat && useJsonPayloadFormatForDefaultDownlinkTopics); transportPayloadTypeConfiguration = protoTransportPayloadConfiguration; } mqttDeviceProfileTransportConfiguration.setTransportPayloadTypeConfiguration(transportPayloadTypeConfiguration); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java index edbe2fcd28..8d2dce0bb9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java @@ -15,24 +15,72 @@ */ package org.thingsboard.server.transport.mqtt.attributes; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; +import com.google.protobuf.InvalidProtocolBufferException; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; +import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; 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; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest { + public static final String ATTRIBUTES_SCHEMA_STR = "syntax =\"proto3\";\n" + + "\n" + + "package test;\n" + + "\n" + + "message PostAttributes {\n" + + " string attribute1 = 1;\n" + + " bool attribute2 = 2;\n" + + " double attribute3 = 3;\n" + + " int32 attribute4 = 4;\n" + + " JsonObject attribute5 = 5;\n" + + "\n" + + " message JsonObject {\n" + + " int32 someNumber = 6;\n" + + " repeated int32 someArray = 7;\n" + + " NestedJsonObject someNestedObject = 8;\n" + + " message NestedJsonObject {\n" + + " string key = 9;\n" + + " }\n" + + " }\n" + + "}"; + protected static final String POST_ATTRIBUTES_PAYLOAD = "{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73," + "\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; + private static final String RESPONSE_ATTRIBUTES_PAYLOAD_DELETED = "{\"deleted\":[\"attribute5\"]}"; + protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception { super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic); } @@ -107,4 +155,544 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt } } + // subscribe to attributes updates from server methods + + protected void processJsonTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateUpdateAttributesJsonResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateDeleteAttributesJsonResponse(onDeleteCallback); + } + + protected void processProtoTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateUpdateAttributesProtoResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateDeleteAttributesProtoResponse(onDeleteCallback); + } + + protected void validateUpdateAttributesJsonResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response)); + } + + protected void validateDeleteAttributesJsonResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response)); + } + + protected void validateUpdateAttributesProtoResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + List actualSharedUpdatedList = actualAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedUpdatedList = expectedAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); + assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); + } + + protected void validateDeleteAttributesProtoResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); + assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + } + + protected void processJsonGatewayTestSubscribeToAttributesUpdates() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + Device device = new Device(); + device.setName("Gateway Device Subscribe to attribute updates"); + device.setType("default"); + + byte[] connectPayloadBytes = getJsonConnectPayloadBytes(); + + publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateJsonGatewayUpdateAttributesResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateJsonGatewayDeleteAttributesResponse(onDeleteCallback); + + } + + protected void processProtoGatewayTestSubscribeToAttributesUpdates() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + TestMqttCallback onUpdateCallback = getTestMqttCallback(); + client.setCallback(onUpdateCallback); + + Device device = new Device(); + device.setName("Gateway Device Subscribe to attribute updates"); + device.setType("default"); + + byte[] connectPayloadBytes = getProtoConnectPayloadBytes(); + + publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateProtoGatewayUpdateAttributesResponse(onUpdateCallback); + + TestMqttCallback onDeleteCallback = getTestMqttCallback(); + client.setCallback(onDeleteCallback); + + doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); + onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); + + validateProtoGatewayDeleteAttributesResponse(onDeleteCallback); + + } + + protected void validateJsonGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(getJsonResponseGatewayAttributesUpdatedPayload(), s); + } + + protected void validateJsonGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); + assertEquals(s, getJsonResponseGatewayAttributesDeletedPayload()); + } + + protected byte[] getJsonConnectPayloadBytes() { + String connectPayload = "{\"device\": \"Gateway Device Subscribe to attribute updates\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; + return connectPayload.getBytes(); + } + + private static String getJsonResponseGatewayAttributesUpdatedPayload() { + return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\"," + + "\"data\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + } + + private static String getJsonResponseGatewayAttributesDeletedPayload() { + return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\",\"data\":{\"deleted\":[\"attribute5\"]}}"; + } + + protected void validateProtoGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(expectedAttributeUpdateNotificationMsg); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); + TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); + + List actualSharedUpdatedList = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedUpdatedList = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); + assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); + + } + + protected void validateProtoGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { + assertNotNull(callback.getPayloadBytes()); + TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); + attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); + TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); + gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); + gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(attributeUpdateNotificationMsg); + + TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); + TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); + + assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); + + TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); + TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); + + assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); + assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); + + } + + protected byte[] getProtoConnectPayloadBytes() { + TransportApiProtos.ConnectMsg connectProto = getConnectProto(); + return connectProto.toByteArray(); + } + + private TransportApiProtos.ConnectMsg getConnectProto() { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName("Gateway Device Subscribe to attribute updates"); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + + // request attributes from server methods + + protected void processJsonTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + postJsonAttributesAndSubscribeToTopic(savedDevice, client, attrPubTopic, attrSubTopic); + + Thread.sleep(5000); + + TestMqttCallback callback = getTestMqttCallback(); + client.setCallback(callback); + + validateJsonResponse(client, callback.getLatch(), callback, attrReqTopicPrefix); + } + + protected void processProtoTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(accessToken); + + postProtoAttributesAndSubscribeToTopic(savedDevice, client, attrPubTopic, attrSubTopic); + + Thread.sleep(5000); + + TestMqttCallback callback = getTestMqttCallback(); + client.setCallback(callback); + + validateProtoResponse(client, callback.getLatch(), callback, attrReqTopicPrefix); + } + + protected void processJsonTestGatewayRequestAttributesValuesFromTheServer() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + postJsonGatewayDeviceClientAttributes(client); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + Thread.sleep(2000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + + Thread.sleep(5000); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + + TestMqttCallback clientAttributesCallback = getTestMqttCallback(); + client.setCallback(clientAttributesCallback); + validateJsonClientResponseGateway(client, clientAttributesCallback); + + TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); + client.setCallback(sharedAttributesCallback); + validateJsonSharedResponseGateway(client, sharedAttributesCallback); + } + + protected void processProtoTestGatewayRequestAttributesValuesFromTheServer() throws Exception { + + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + + postProtoGatewayDeviceClientAttributes(client); + + Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), + 20, + 100); + + assertNotNull(savedDevice); + + Thread.sleep(2000); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + + Thread.sleep(5000); + + client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + + TestMqttCallback clientAttributesCallback = getTestMqttCallback(); + client.setCallback(clientAttributesCallback); + validateProtoClientResponseGateway(client, clientAttributesCallback); + + TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); + client.setCallback(sharedAttributesCallback); + validateProtoSharedResponseGateway(client, sharedAttributesCallback); + } + + protected void postJsonAttributesAndSubscribeToTopic(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(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 postProtoAttributesAndSubscribeToTopic(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); + MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(ATTRIBUTES_SCHEMA_STR); + DynamicSchema attributesSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, ProtoTransportPayloadConfiguration.ATTRIBUTES_PROTO_SCHEMA); + + DynamicMessage.Builder nestedJsonObjectBuilder = attributesSchema.newMessageBuilder("PostAttributes.JsonObject.NestedJsonObject"); + Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); + assertNotNull(nestedJsonObjectBuilderDescriptor); + DynamicMessage nestedJsonObject = nestedJsonObjectBuilder.setField(nestedJsonObjectBuilderDescriptor.findFieldByName("key"), "value").build(); + + DynamicMessage.Builder jsonObjectBuilder = attributesSchema.newMessageBuilder("PostAttributes.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 postAttributesBuilder = attributesSchema.newMessageBuilder("PostAttributes"); + Descriptors.Descriptor postAttributesMsgDescriptor = postAttributesBuilder.getDescriptorForType(); + assertNotNull(postAttributesMsgDescriptor); + DynamicMessage postAttributesMsg = postAttributesBuilder + .setField(postAttributesMsgDescriptor.findFieldByName("attribute1"), "value1") + .setField(postAttributesMsgDescriptor.findFieldByName("attribute2"), true) + .setField(postAttributesMsgDescriptor.findFieldByName("attribute3"), 42.0) + .setField(postAttributesMsgDescriptor.findFieldByName("attribute4"), 73) + .setField(postAttributesMsgDescriptor.findFieldByName("attribute5"), jsonObject) + .build(); + byte[] payload = postAttributesMsg.toByteArray(); + client.publish(attrPubTopic, new MqttMessage(payload)); + client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); + } + + protected void postJsonGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { + String postClientAttributes = "{\"" + "Gateway Device Request Attributes" + "\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + } + + protected void postProtoGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + List expectedKeys = Arrays.asList(keys.split(",")); + TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); + TransportApiProtos.AttributesMsg.Builder attributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + attributesMsgBuilder.setDeviceName("Gateway Device Request Attributes"); + attributesMsgBuilder.setMsg(postAttributeMsg); + TransportApiProtos.AttributesMsg attributesMsg = attributesMsgBuilder.build(); + TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributeMsgBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); + gatewayAttributeMsgBuilder.addMsg(attributesMsg); + byte[] bytes = gatewayAttributeMsgBuilder.build().toByteArray(); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes)); + } + + protected void validateJsonResponse(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(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\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateProtoResponse(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); + attributesRequestBuilder.setSharedKeys(keys); + TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(attributesRequest.toByteArray()); + client.publish(attrReqTopic + "1", mqttMessage); + latch.await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedAttributesResponse.getRequestId(), actualAttributesResponse.getRequestId()); + List expectedClientKeyValueProtos = expectedAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List expectedSharedKeyValueProtos = expectedAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualClientKeyValueProtos = actualAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualSharedKeyValueProtos = actualAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); + assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); + } + + private TransportProtos.GetAttributeResponseMsg getExpectedAttributeResponseMsg() { + TransportProtos.GetAttributeResponseMsg.Builder result = TransportProtos.GetAttributeResponseMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + result.addAllClientAttributeList(tsKvProtoList); + result.addAllSharedAttributeList(tsKvProtoList); + result.setRequestId(1); + return result.build(); + } + + protected void validateJsonClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": true, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + callback.getLatch().await(1, TimeUnit.MINUTES); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateJsonSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": false, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; + MqttMessage mqttMessage = new MqttMessage(); + mqttMessage.setPayload(payloadStr.getBytes()); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); + callback.getLatch().await(1, TimeUnit.MINUTES); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; + assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + } + + protected void validateProtoClientResponseGateway(MqttAsyncClient client, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, true); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(true); + TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); + + TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); + assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); + + List expectedClientKeyValueProtos = expectedResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualClientKeyValueProtos = actualResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); + } + + protected void validateProtoSharedResponseGateway(MqttAsyncClient client, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { + String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, false); + client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); + callback.getLatch().await(3, TimeUnit.SECONDS); + assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); + TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(false); + TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); + assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); + + TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); + TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); + assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); + + List expectedSharedKeyValueProtos = expectedResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + List actualSharedKeyValueProtos = actualResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); + + assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); + } + + private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(boolean client) { + TransportApiProtos.GatewayAttributeResponseMsg.Builder gatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); + TransportProtos.GetAttributeResponseMsg.Builder getAttributeResponseMsgBuilder = TransportProtos.GetAttributeResponseMsg.newBuilder(); + List tsKvProtoList = getTsKvProtoList(); + if (client) { + getAttributeResponseMsgBuilder.addAllClientAttributeList(tsKvProtoList); + } else { + getAttributeResponseMsgBuilder.addAllSharedAttributeList(tsKvProtoList); + } + getAttributeResponseMsgBuilder.setRequestId(1); + TransportProtos.GetAttributeResponseMsg getAttributeResponseMsg = getAttributeResponseMsgBuilder.build(); + gatewayAttributeResponseMsg.setDeviceName("Gateway Device Request Attributes"); + gatewayAttributeResponseMsg.setResponseMsg(getAttributeResponseMsg); + return gatewayAttributeResponseMsg.build(); + } + + private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String keys, boolean client) { + return TransportApiProtos.GatewayAttributesRequestMsg.newBuilder() + .setClient(client) + .addAllKeys(Arrays.asList(keys.split(","))) + .setDeviceName("Gateway Device Request Attributes") + .setId(1).build(); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestBackwardCompatibilityIntegrationTest.java new file mode 100644 index 0000000000..b905d91a17 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/AbstractMqttAttributesRequestBackwardCompatibilityIntegrationTest.java @@ -0,0 +1,94 @@ +/** + * 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.transport.mqtt.attributes.request; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Test; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public abstract class AbstractMqttAttributesRequestBackwardCompatibilityIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testRequestAttributesValuesFromTheServerWithEnabledJsonCompatibility() 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, true, false); + processProtoTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerWithEnabledJsonCompatibilityAndJsonDownlinks() 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, true, true); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortTopicWithEnabledJsonCompatibilityAndJsonDownlinks() 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, true, true); + processProtoTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortProtoTopicWithEnabledJsonCompatibilityAndJsonDownlinks() 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, true, true); + processProtoTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX); + } + + @Test + public void testRequestAttributesValuesFromTheServerGatewayWithEnabledJsonCompatibilityAndJsonDownlinks() 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, true, true); + processProtoTestGatewayRequestAttributesValuesFromTheServer(); + } + + @Test + public void testRequestAttributesValuesFromTheServerOnShortJsonTopicWithEnabledJsonCompatibilityAndJsonDownlinks() 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, true, true); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); + } + + + protected List getKvProtos(List expectedKeys) { + List keyValueProtos = new ArrayList<>(); + TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V); + TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V); + TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "42.0", TransportProtos.KeyValueType.DOUBLE_V); + TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "73", TransportProtos.KeyValueType.LONG_V); + TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V); + keyValueProtos.add(strKeyValueProto); + keyValueProtos.add(boolKeyValueProto); + keyValueProtos.add(dblKeyValueProto); + keyValueProtos.add(longKeyValueProto); + keyValueProtos.add(jsonKeyValueProto); + return keyValueProtos; + } + +} 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 c54e76e4a9..ea3c797b27 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 @@ -15,7 +15,11 @@ */ package org.thingsboard.server.transport.mqtt.attributes.request; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; @@ -25,16 +29,26 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Device; +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.common.util.JacksonUtil; +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; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j @@ -52,109 +66,21 @@ public abstract class AbstractMqttAttributesRequestIntegrationTest extends Abstr @Test public void testRequestAttributesValuesFromTheServer() throws Exception { - processTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + processJsonTestRequestAttributesValuesFromTheServer(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); + processJsonTestRequestAttributesValuesFromTheServer(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); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); } @Test public void testRequestAttributesValuesFromTheServerGateway() throws Exception { - processTestGatewayRequestAttributesValuesFromTheServer(); - } - - protected void processTestRequestAttributesValuesFromTheServer(String attrPubTopic, String attrSubTopic, String attrReqTopicPrefix) throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - postAttributesAndSubscribeToTopic(savedDevice, client, attrPubTopic, attrSubTopic); - - Thread.sleep(5000); - - TestMqttCallback callback = getTestMqttCallback(); - client.setCallback(callback); - - validateResponse(client, callback.getLatch(), callback, attrReqTopicPrefix); - } - - protected void processTestGatewayRequestAttributesValuesFromTheServer() throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - - postGatewayDeviceClientAttributes(client); - - Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class), - 20, - 100); - - assertNotNull(savedDevice); - - Thread.sleep(2000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - - Thread.sleep(5000); - - client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value()).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - - TestMqttCallback clientAttributesCallback = getTestMqttCallback(); - client.setCallback(clientAttributesCallback); - validateClientResponseGateway(client, clientAttributesCallback); - - TestMqttCallback sharedAttributesCallback = getTestMqttCallback(); - client.setCallback(sharedAttributesCallback); - validateSharedResponseGateway(client, sharedAttributesCallback); - } - - 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(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 { - String postClientAttributes = "{\"" + "Gateway Device Request Attributes" + "\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes())).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - } - - 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(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\"}}}}"; - assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); - } - - protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": true, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(payloadStr.getBytes()); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - callback.getLatch().await(1, TimeUnit.MINUTES); - assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); - } - - protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": false, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}"; - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(payloadStr.getBytes()); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage).waitForCompletion(TimeUnit.MINUTES.toMillis(1)); - callback.getLatch().await(1, TimeUnit.MINUTES); - assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8))); + processJsonTestGatewayRequestAttributesValuesFromTheServer(); } } 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 2bd1877ee1..9bb6b45507 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 @@ -21,9 +21,10 @@ 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.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; @Slf4j -public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { +public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesIntegrationTest { @Before public void beforeTest() throws Exception { @@ -37,21 +38,21 @@ public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends A @Test public void testRequestAttributesValuesFromTheServer() throws Exception { - super.testRequestAttributesValuesFromTheServer(); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); } @Test public void testRequestAttributesValuesFromTheServerOnShortTopic() throws Exception { - super.testRequestAttributesValuesFromTheServerOnShortTopic(); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_TOPIC_PREFIX); } @Test public void testRequestAttributesValuesFromTheServerOnShortJsonTopic() throws Exception { - super.testRequestAttributesValuesFromTheServerOnShortJsonTopic(); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); } @Test public void testRequestAttributesValuesFromTheServerGateway() throws Exception { - processTestGatewayRequestAttributesValuesFromTheServer(); + processJsonTestGatewayRequestAttributesValuesFromTheServer(); } } 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 3015d91ffb..327819a8c5 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 @@ -15,65 +15,20 @@ */ package org.thingsboard.server.transport.mqtt.attributes.request; -import com.github.os72.protobuf.dynamic.DynamicSchema; -import com.google.protobuf.Descriptors; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.InvalidProtocolBufferException; -import com.squareup.wire.schema.internal.parser.ProtoFileElement; -import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; import org.junit.After; import org.junit.Test; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfileProvisionType; 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.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.MqttTopics; -import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; -import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j -public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { - - public static final String ATTRIBUTES_SCHEMA_STR = "syntax =\"proto3\";\n" + - "\n" + - "package test;\n" + - "\n" + - "message PostAttributes {\n" + - " string attribute1 = 1;\n" + - " bool attribute2 = 2;\n" + - " double attribute3 = 3;\n" + - " int32 attribute4 = 4;\n" + - " JsonObject attribute5 = 5;\n" + - "\n" + - " message JsonObject {\n" + - " int32 someNumber = 6;\n" + - " repeated int32 someArray = 7;\n" + - " NestedJsonObject someNestedObject = 8;\n" + - " message NestedJsonObject {\n" + - " string key = 9;\n" + - " }\n" + - " }\n" + - "}"; +public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttributesIntegrationTest { @After public void afterTest() throws Exception { @@ -83,182 +38,36 @@ public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends @Test 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(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX); + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); + processProtoTestRequestAttributesValuesFromTheServer(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); + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); + processProtoTestRequestAttributesValuesFromTheServer(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); + TransportPayloadType.PROTOBUF, null, null, null, ATTRIBUTES_SCHEMA_STR, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); + processProtoTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_PROTO_TOPIC_PREFIX); } @Test public void testRequestAttributesValuesFromTheServerGateway() 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); - processTestGatewayRequestAttributesValuesFromTheServer(); + processProtoTestGatewayRequestAttributesValuesFromTheServer(); } @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); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - ProtoFileElement transportProtoSchema = protoTransportPayloadConfiguration.getTransportProtoSchema(ATTRIBUTES_SCHEMA_STR); - DynamicSchema attributesSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchema, ProtoTransportPayloadConfiguration.ATTRIBUTES_PROTO_SCHEMA); - - DynamicMessage.Builder nestedJsonObjectBuilder = attributesSchema.newMessageBuilder("PostAttributes.JsonObject.NestedJsonObject"); - Descriptors.Descriptor nestedJsonObjectBuilderDescriptor = nestedJsonObjectBuilder.getDescriptorForType(); - assertNotNull(nestedJsonObjectBuilderDescriptor); - DynamicMessage nestedJsonObject = nestedJsonObjectBuilder.setField(nestedJsonObjectBuilderDescriptor.findFieldByName("key"), "value").build(); - - DynamicMessage.Builder jsonObjectBuilder = attributesSchema.newMessageBuilder("PostAttributes.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 postAttributesBuilder = attributesSchema.newMessageBuilder("PostAttributes"); - Descriptors.Descriptor postAttributesMsgDescriptor = postAttributesBuilder.getDescriptorForType(); - assertNotNull(postAttributesMsgDescriptor); - DynamicMessage postAttributesMsg = postAttributesBuilder - .setField(postAttributesMsgDescriptor.findFieldByName("attribute1"), "value1") - .setField(postAttributesMsgDescriptor.findFieldByName("attribute2"), true) - .setField(postAttributesMsgDescriptor.findFieldByName("attribute3"), 42.0) - .setField(postAttributesMsgDescriptor.findFieldByName("attribute4"), 73) - .setField(postAttributesMsgDescriptor.findFieldByName("attribute5"), jsonObject) - .build(); - byte[] payload = postAttributesMsg.toByteArray(); - client.publish(attrPubTopic, new MqttMessage(payload)); - client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); - } - - protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - List expectedKeys = Arrays.asList(keys.split(",")); - TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys); - TransportApiProtos.AttributesMsg.Builder attributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); - attributesMsgBuilder.setDeviceName("Gateway Device Request Attributes"); - attributesMsgBuilder.setMsg(postAttributeMsg); - TransportApiProtos.AttributesMsg attributesMsg = attributesMsgBuilder.build(); - TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributeMsgBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder(); - gatewayAttributeMsgBuilder.addMsg(attributesMsg); - byte[] bytes = gatewayAttributeMsgBuilder.build().toByteArray(); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes)); - } - - 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); - attributesRequestBuilder.setSharedKeys(keys); - TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build(); - MqttMessage mqttMessage = new MqttMessage(); - mqttMessage.setPayload(attributesRequest.toByteArray()); - client.publish(attrReqTopic + "1", mqttMessage); - latch.await(3, TimeUnit.SECONDS); - assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); - TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg(); - TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); - assertEquals(expectedAttributesResponse.getRequestId(), actualAttributesResponse.getRequestId()); - List expectedClientKeyValueProtos = expectedAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List expectedSharedKeyValueProtos = expectedAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List actualClientKeyValueProtos = actualAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List actualSharedKeyValueProtos = actualAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); - assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); - } - - protected void validateClientResponseGateway(MqttAsyncClient client, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, true); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); - callback.getLatch().await(3, TimeUnit.SECONDS); - assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(true); - TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); - assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); - - TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); - TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); - assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); - - List expectedClientKeyValueProtos = expectedResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List actualClientKeyValueProtos = actualResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos)); - } - - protected void validateSharedResponseGateway(MqttAsyncClient client, AbstractMqttAttributesIntegrationTest.TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException { - String keys = "attribute1,attribute2,attribute3,attribute4,attribute5"; - TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, false); - client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray())); - callback.getLatch().await(3, TimeUnit.SECONDS); - assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS()); - TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(false); - TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes()); - assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName()); - - TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg(); - TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg(); - assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId()); - - List expectedSharedKeyValueProtos = expectedResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List actualSharedKeyValueProtos = actualResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - - assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos)); - } - - private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String keys, boolean client) { - return TransportApiProtos.GatewayAttributesRequestMsg.newBuilder() - .setClient(client) - .addAllKeys(Arrays.asList(keys.split(","))) - .setDeviceName("Gateway Device Request Attributes") - .setId(1).build(); - } - - private TransportProtos.GetAttributeResponseMsg getExpectedAttributeResponseMsg() { - TransportProtos.GetAttributeResponseMsg.Builder result = TransportProtos.GetAttributeResponseMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); - result.addAllClientAttributeList(tsKvProtoList); - result.addAllSharedAttributeList(tsKvProtoList); - result.setRequestId(1); - return result.build(); + public void testRequestAttributesValuesFromTheServerOnShortJsonTopic() 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); + processJsonTestRequestAttributesValuesFromTheServer(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_ATTRIBUTES_REQUEST_SHORT_JSON_TOPIC_PREFIX); } - private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(boolean client) { - TransportApiProtos.GatewayAttributeResponseMsg.Builder gatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder(); - TransportProtos.GetAttributeResponseMsg.Builder getAttributeResponseMsgBuilder = TransportProtos.GetAttributeResponseMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); - if (client) { - getAttributeResponseMsgBuilder.addAllClientAttributeList(tsKvProtoList); - } else { - getAttributeResponseMsgBuilder.addAllSharedAttributeList(tsKvProtoList); - } - getAttributeResponseMsgBuilder.setRequestId(1); - TransportProtos.GetAttributeResponseMsg getAttributeResponseMsg = getAttributeResponseMsgBuilder.build(); - gatewayAttributeResponseMsg.setDeviceName("Gateway Device Request Attributes"); - gatewayAttributeResponseMsg.setResponseMsg(getAttributeResponseMsg); - return gatewayAttributeResponseMsg.build(); - } protected List getKvProtos(List expectedKeys) { List keyValueProtos = new ArrayList<>(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java new file mode 100644 index 0000000000..166d2d7b14 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestBackwardCompatibilityIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * 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.transport.mqtt.attributes.request.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.attributes.request.AbstractMqttAttributesRequestBackwardCompatibilityIntegrationTest; + +@DaoSqlTest +public class MqttAttributesRequestBackwardCompatibilityIntegrationTest extends AbstractMqttAttributesRequestBackwardCompatibilityIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestIntegrationTest.java index 89253462f9..f56faeacac 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest; @DaoSqlTest -public class MqttAttributesRequestSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { +public class MqttAttributesRequestIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestJsonIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestJsonIntegrationTest.java index b6540832be..dbadaa4f1c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestJsonIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest; @DaoSqlTest -public class MqttAttributesRequestJsonSqlIntegrationTest extends AbstractMqttAttributesRequestJsonIntegrationTest { +public class MqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesRequestJsonIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestProtoIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestProtoIntegrationTest.java index 4615b75824..e1b58b9a5b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/request/sql/MqttAttributesRequestProtoIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.attributes.request.AbstractMqttAttributesRequestProtoIntegrationTest; @DaoSqlTest -public class MqttAttributesRequestProtoSqlIntegrationTest extends AbstractMqttAttributesRequestProtoIntegrationTest { +public class MqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttributesRequestProtoIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java new file mode 100644 index 0000000000..a54bbe3688 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/AbstractMqttAttributesUpdatesBackwardCompatibilityIntegrationTest.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.transport.mqtt.attributes.updates; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; + +@Slf4j +public abstract class AbstractMqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends AbstractMqttAttributesIntegrationTest { + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testSubscribeToAttributesUpdatesFromServerWithEnabledJsonCompatibility() throws Exception { + super.processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null, true, false); + processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + } + + @Test + public void testSubscribeToAttributesUpdatesFromServerWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null, true, true); + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + } + + @Test + public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null, true, true); + processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + } + + @Test + public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null, true, true); + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); + } + + @Test + public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortProtoTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null, true, true); + processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); + } + + @Test + public void testProtoSubscribeToAttributesUpdatesFromTheServerGatewayWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null, true, false); + processProtoGatewayTestSubscribeToAttributesUpdates(); + } + +} 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 f179741abb..6973f7db8f 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 @@ -15,40 +15,17 @@ */ package org.thingsboard.server.transport.mqtt.attributes.updates; -import com.google.protobuf.InvalidProtocolBufferException; -import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @Slf4j public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributesIntegrationTest { - private static final String RESPONSE_ATTRIBUTES_PAYLOAD_DELETED = "{\"deleted\":[\"attribute5\"]}"; - - private static String getResponseGatewayAttributesUpdatedPayload() { - return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\"," + - "\"data\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}"; - } - - private static String getResponseGatewayAttributesDeletedPayload() { - return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\",\"data\":{\"deleted\":[\"attribute5\"]}}"; - } - @Before public void beforeTest() throws Exception { processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null); @@ -60,116 +37,23 @@ public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends Abstr } @Test - public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { - processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); + public void testJsonSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { - processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); + public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { - processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); + public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { - processGatewayTestSubscribeToAttributesUpdates(); - } - - protected void processTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(accessToken); - - TestMqttCallback onUpdateCallback = getTestMqttCallback(); - client.setCallback(onUpdateCallback); - - client.subscribe(attrSubTopic, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateUpdateAttributesResponse(onUpdateCallback); - - TestMqttCallback onDeleteCallback = getTestMqttCallback(); - client.setCallback(onDeleteCallback); - - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); - onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateDeleteAttributesResponse(onDeleteCallback); - } - - protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response)); - } - - protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response)); + public void testJsonSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processJsonGatewayTestSubscribeToAttributesUpdates(); } - protected void processGatewayTestSubscribeToAttributesUpdates() throws Exception { - - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - - TestMqttCallback onUpdateCallback = getTestMqttCallback(); - client.setCallback(onUpdateCallback); - - Device device = new Device(); - device.setName("Gateway Device Subscribe to attribute updates"); - device.setType("default"); - - byte[] connectPayloadBytes = getConnectPayloadBytes(); - - publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); - - Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class), - 20, - 100); - - assertNotNull(savedDevice); - - client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value()); - - Thread.sleep(1000); - - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); - onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateGatewayUpdateAttributesResponse(onUpdateCallback); - - TestMqttCallback onDeleteCallback = getTestMqttCallback(); - client.setCallback(onDeleteCallback); - - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class); - onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS); - - validateGatewayDeleteAttributesResponse(onDeleteCallback); - - } - - protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(getResponseGatewayAttributesUpdatedPayload(), s); - } - - protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8); - assertEquals(s, getResponseGatewayAttributesDeletedPayload()); - } - - protected byte[] getConnectPayloadBytes() { - String connectPayload = "{\"device\": \"Gateway Device Subscribe to attribute updates\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; - return connectPayload.getBytes(); - } } 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 4efadaccaa..02dbe8e8a8 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 @@ -20,9 +20,11 @@ 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.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; @Slf4j -public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { +public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesIntegrationTest { @Before public void beforeTest() throws Exception { @@ -35,22 +37,22 @@ public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends A } @Test - public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { - super.testSubscribeToAttributesUpdatesFromTheServer(); + public void testJsonSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { - super.testSubscribeToAttributesUpdatesFromTheServerOnShortTopic(); + public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { - super.testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic(); + public void testJsonSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { - processGatewayTestSubscribeToAttributesUpdates(); + public void testJsonSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processJsonGatewayTestSubscribeToAttributesUpdates(); } } 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 606e05930d..5c0bc631bb 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 @@ -24,6 +24,7 @@ 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; +import org.thingsboard.server.transport.mqtt.attributes.AbstractMqttAttributesIntegrationTest; import java.util.List; import java.util.stream.Collectors; @@ -33,7 +34,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @Slf4j -public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { +public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesIntegrationTest { @Before public void beforeTest() throws Exception { @@ -46,116 +47,28 @@ public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends } @Test - public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception { - super.testSubscribeToAttributesUpdatesFromTheServer(); + public void testProtoSubscribeToAttributesUpdatesFromTheServer() throws Exception { + processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { - super.testSubscribeToAttributesUpdatesFromTheServerOnShortTopic(); + public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortTopic() throws Exception { + processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception {} - - @Test - public void testSubscribeToAttributesUpdatesFromTheServerOnShortProtoTopic() throws Exception { - processTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); + public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortJsonTopic() throws Exception { + processJsonTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC); } @Test - public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { - processGatewayTestSubscribeToAttributesUpdates(); + public void testProtoSubscribeToAttributesUpdatesFromTheServerOnShortProtoTopic() throws Exception { + processProtoTestSubscribeToAttributesUpdates(MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC); } - protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); - attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); - - TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); - TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); - - List actualSharedUpdatedList = actualAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List expectedSharedUpdatedList = expectedAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - - assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); - assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); - - } - - protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); - - TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); - TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); - - assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); - assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); - - } - - protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - - TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - List tsKvProtoList = getTsKvProtoList(); - attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList); - TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); - - TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); - gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); - gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(expectedAttributeUpdateNotificationMsg); - - TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); - TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); - - assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); - - List actualSharedUpdatedList = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - List expectedSharedUpdatedList = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList()); - - assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size()); - assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList)); - - } - - protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException { - assertNotNull(callback.getPayloadBytes()); - TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder(); - attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5"); - TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build(); - - TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder(); - gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates"); - gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(attributeUpdateNotificationMsg); - - TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build(); - TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes()); - - assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName()); - - TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); - TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg(); - - assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size()); - assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0)); - - } - - protected byte[] getConnectPayloadBytes() { - TransportApiProtos.ConnectMsg connectProto = getConnectProto(); - return connectProto.toByteArray(); - } - - private TransportApiProtos.ConnectMsg getConnectProto() { - TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); - builder.setDeviceName("Gateway Device Subscribe to attribute updates"); - builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); - return builder.build(); + @Test + public void testProtoSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception { + processProtoGatewayTestSubscribeToAttributesUpdates(); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java new file mode 100644 index 0000000000..94b0284ece --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesBackwardCompatibilityIntegrationTest.java @@ -0,0 +1,23 @@ +/** + * 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.transport.mqtt.attributes.updates.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.attributes.updates.AbstractMqttAttributesUpdatesBackwardCompatibilityIntegrationTest; + +@DaoSqlTest +public class MqttAttributesUpdatesBackwardCompatibilityIntegrationTest extends AbstractMqttAttributesUpdatesBackwardCompatibilityIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesIntegrationTest.java index 0f5dfdda46..fcc3dd0e92 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest; @DaoSqlTest -public class MqttAttributesUpdatesSqlIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { +public class MqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesJsonIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesJsonIntegrationTest.java index 54b5060886..51aaeb60d6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesJsonIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest; @DaoSqlTest -public class MqttAttributesUpdatesSqlJsonIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest { +public class MqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesProtoIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesProtoIntegrationTest.java index 68267bbd49..0139c7c547 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/attributes/updates/sql/MqttAttributesUpdatesProtoIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.attributes.updates.AbstractMqttAttributesUpdatesProtoIntegrationTest; @DaoSqlTest -public class MqttAttributesUpdatesSqlProtoIntegrationTest extends AbstractMqttAttributesUpdatesProtoIntegrationTest { +public class MqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesUpdatesProtoIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimBackwardCompatibilityDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimBackwardCompatibilityDeviceTest.java new file mode 100644 index 0000000000..2c0fa6c877 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimBackwardCompatibilityDeviceTest.java @@ -0,0 +1,50 @@ +/** + * 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.transport.mqtt.claim; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.TransportPayloadType; + +@Slf4j +public abstract class AbstractMqttClaimBackwardCompatibilityDeviceTest extends AbstractMqttClaimDeviceTest { + + @Before + public void beforeTest() throws Exception { + processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.PROTOBUF, null, null, true, true); + createCustomerAndUser(); + } + + @After + public void afterTest() throws Exception { super.afterTest(); } + + @Test + public void testGatewayClaimingDevice() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device Proto", false); + } + + @Test + public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception { + processTestGatewayClaimingDevice("Test claiming gateway device empty payload Proto", true); + } + + protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { + processProtoTestGatewayClaimDevice(deviceName, emptyPayload); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimDeviceTest.java index 6bbcf82e39..996e460f22 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimDeviceTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.device.claim.ClaimResponse; import org.thingsboard.server.dao.device.claim.ClaimResult; +import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import static org.junit.Assert.assertEquals; @@ -202,4 +203,36 @@ public abstract class AbstractMqttClaimDeviceTest extends AbstractMqttIntegratio validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); } + protected void processProtoTestGatewayClaimDevice(String deviceName, boolean emptyPayload) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + byte[] failurePayloadBytes; + byte[] payloadBytes; + if (emptyPayload) { + payloadBytes = getGatewayClaimMsg(deviceName, 0, emptyPayload).toByteArray(); + } else { + payloadBytes = getGatewayClaimMsg(deviceName, 60000, emptyPayload).toByteArray(); + } + failurePayloadBytes = getGatewayClaimMsg(deviceName, 1, emptyPayload).toByteArray(); + + validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); + } + + private TransportApiProtos.GatewayClaimMsg getGatewayClaimMsg(String deviceName, long duration, boolean emptyPayload) { + TransportApiProtos.GatewayClaimMsg.Builder gatewayClaimMsgBuilder = TransportApiProtos.GatewayClaimMsg.newBuilder(); + TransportApiProtos.ClaimDeviceMsg.Builder claimDeviceMsgBuilder = TransportApiProtos.ClaimDeviceMsg.newBuilder(); + TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder(); + if (!emptyPayload) { + claimDeviceBuilder.setSecretKey("value"); + } + if (duration > 0) { + claimDeviceBuilder.setDurationMs(duration); + } + TransportApiProtos.ClaimDevice claimDevice = claimDeviceBuilder.build(); + claimDeviceMsgBuilder.setClaimRequest(claimDevice); + claimDeviceMsgBuilder.setDeviceName(deviceName); + TransportApiProtos.ClaimDeviceMsg claimDeviceMsg = claimDeviceMsgBuilder.build(); + gatewayClaimMsgBuilder.addMsg(claimDeviceMsg); + return gatewayClaimMsgBuilder.build(); + } + } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java index c3a5fb8cbb..5dc5bcf8be 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java @@ -68,35 +68,7 @@ public abstract class AbstractMqttClaimProtoDeviceTest extends AbstractMqttClaim } protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - byte[] failurePayloadBytes; - byte[] payloadBytes; - if (emptyPayload) { - payloadBytes = getGatewayClaimMsg(deviceName, 0, emptyPayload).toByteArray(); - } else { - payloadBytes = getGatewayClaimMsg(deviceName, 60000, emptyPayload).toByteArray(); - } - failurePayloadBytes = getGatewayClaimMsg(deviceName, 1, emptyPayload).toByteArray(); - - validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes); - } - - private TransportApiProtos.GatewayClaimMsg getGatewayClaimMsg(String deviceName, long duration, boolean emptyPayload) { - TransportApiProtos.GatewayClaimMsg.Builder gatewayClaimMsgBuilder = TransportApiProtos.GatewayClaimMsg.newBuilder(); - TransportApiProtos.ClaimDeviceMsg.Builder claimDeviceMsgBuilder = TransportApiProtos.ClaimDeviceMsg.newBuilder(); - TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder(); - if (!emptyPayload) { - claimDeviceBuilder.setSecretKey("value"); - } - if (duration > 0) { - claimDeviceBuilder.setDurationMs(duration); - } - TransportApiProtos.ClaimDevice claimDevice = claimDeviceBuilder.build(); - claimDeviceMsgBuilder.setClaimRequest(claimDevice); - claimDeviceMsgBuilder.setDeviceName(deviceName); - TransportApiProtos.ClaimDeviceMsg claimDeviceMsg = claimDeviceMsgBuilder.build(); - gatewayClaimMsgBuilder.addMsg(claimDeviceMsg); - return gatewayClaimMsgBuilder.build(); + processProtoTestGatewayClaimDevice(deviceName, emptyPayload); } private TransportApiProtos.ClaimDevice getClaimDevice(long duration, boolean emptyPayload) { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceBackwardCompatibilityTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceBackwardCompatibilityTest.java new file mode 100644 index 0000000000..3135aa951c --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceBackwardCompatibilityTest.java @@ -0,0 +1,24 @@ +/** + * 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.transport.mqtt.claim.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.claim.AbstractMqttClaimBackwardCompatibilityDeviceTest; +import org.thingsboard.server.transport.mqtt.claim.AbstractMqttClaimDeviceTest; + +@DaoSqlTest +public class MqttClaimDeviceBackwardCompatibilityTest extends AbstractMqttClaimBackwardCompatibilityDeviceTest { +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceJsonTest.java similarity index 90% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceJsonTest.java index 18d504f94c..6206f9dc2b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceJsonTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.claim.AbstractMqttClaimJsonDeviceTest; @DaoSqlTest -public class MqttClaimDeviceJsonSqlTest extends AbstractMqttClaimJsonDeviceTest { +public class MqttClaimDeviceJsonTest extends AbstractMqttClaimJsonDeviceTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceProtoTest.java similarity index 90% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceProtoTest.java index fd3595f543..4c35df2e7a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceProtoTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.claim.AbstractMqttClaimProtoDeviceTest; @DaoSqlTest -public class MqttClaimDeviceProtoSqlTest extends AbstractMqttClaimProtoDeviceTest { +public class MqttClaimDeviceProtoTest extends AbstractMqttClaimProtoDeviceTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceSqlTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceTest.java similarity index 91% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceSqlTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceTest.java index 666c51394d..3ab5c72f43 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/claim/sql/MqttClaimDeviceTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.claim.AbstractMqttClaimDeviceTest; @DaoSqlTest -public class MqttClaimDeviceSqlTest extends AbstractMqttClaimDeviceTest { +public class MqttClaimDeviceTest extends AbstractMqttClaimDeviceTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/sql/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/sql/BasicMqttCredentialsTest.java new file mode 100644 index 0000000000..6bb8949f73 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/sql/BasicMqttCredentialsTest.java @@ -0,0 +1,226 @@ +/** + * 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.transport.mqtt.credentials.sql; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.RandomStringUtils; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttSecurityException; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; +import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { + + public static final String CLIENT_ID = "ClientId"; + public static final String USER_NAME1 = "UserName1"; + public static final String USER_NAME2 = "UserName2"; + public static final String USER_NAME3 = "UserName3"; + public static final String PASSWORD = "secret"; + + private Device clientIdDevice; + private Device clientIdAndUserNameDevice1; + private Device clientIdAndUserNameAndPasswordDevice2; + private Device clientIdAndUserNameAndPasswordDevice3; + private Device accessTokenDevice; + private Device accessToken2Device; + + + @Before + public void before() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + + BasicMqttCredentials credValue = new BasicMqttCredentials(); + credValue.setClientId(CLIENT_ID); + clientIdDevice = createDevice("clientIdDevice", credValue); + + credValue = new BasicMqttCredentials(); + credValue.setClientId(CLIENT_ID); + credValue.setUserName(USER_NAME1); + clientIdAndUserNameDevice1 = createDevice("clientIdAndUserNameDevice", credValue); + + credValue = new BasicMqttCredentials(); + credValue.setClientId(CLIENT_ID); + credValue.setUserName(USER_NAME2); + credValue.setPassword(PASSWORD); + clientIdAndUserNameAndPasswordDevice2 = createDevice("clientIdAndUserNameAndPasswordDevice", credValue); + + credValue = new BasicMqttCredentials(); + credValue.setClientId(CLIENT_ID); + credValue.setUserName(USER_NAME3); + credValue.setPassword(PASSWORD); + clientIdAndUserNameAndPasswordDevice3 = createDevice("clientIdAndUserNameAndPasswordDevice2", credValue); + + accessTokenDevice = createDevice("accessTokenDevice", USER_NAME1); + accessToken2Device = createDevice("accessToken2Device", USER_NAME2); + } + + @Test + public void testCorrectCredentials() throws Exception { + // Check that correct devices receive telemetry + testTelemetryIsDelivered(accessTokenDevice, getMqttAsyncClient(null, USER_NAME1, null)); + testTelemetryIsDelivered(clientIdDevice, getMqttAsyncClient(CLIENT_ID, null, null)); + testTelemetryIsDelivered(clientIdAndUserNameDevice1, getMqttAsyncClient(CLIENT_ID, USER_NAME1, null)); + testTelemetryIsDelivered(clientIdAndUserNameAndPasswordDevice2, getMqttAsyncClient(CLIENT_ID, USER_NAME2, PASSWORD)); + + // Also correct. Random clientId and password, but matches access token + testTelemetryIsDelivered(accessToken2Device, getMqttAsyncClient(RandomStringUtils.randomAlphanumeric(10), USER_NAME2, RandomStringUtils.randomAlphanumeric(10))); + } + + @Test(expected = MqttSecurityException.class) + public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception { + // Not correct. Correct clientId and username, but wrong password + testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, getMqttAsyncClient(CLIENT_ID, USER_NAME3, "WRONG PASSWORD")); + } + + private void testTelemetryIsDelivered(Device device, MqttAsyncClient client) throws Exception { + testTelemetryIsDelivered(device, client, true); + } + + private void testTelemetryIsNotDelivered(Device device, MqttAsyncClient client) throws Exception { + testTelemetryIsDelivered(device, client, false); + } + + private void testTelemetryIsDelivered(Device device, MqttAsyncClient client, boolean ok) throws Exception { + String randomKey = RandomStringUtils.randomAlphanumeric(10); + List expectedKeys = Arrays.asList(randomKey); + publishMqttMsg(client, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes(), MqttTopics.DEVICE_TELEMETRY_TOPIC); + + String deviceId = device.getId().getId().toString(); + + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", new TypeReference<>() { + }); + if (actualKeys.size() == expectedKeys.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + if (ok) { + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + Set expectedKeySet = new HashSet<>(expectedKeys); + + assertEquals(expectedKeySet, actualKeySet); + } else { + assertNull(actualKeys); + } + client.disconnect().waitForCompletion(); + } + + @After + public void after() throws Exception { + processAfterTest(); + } + + protected MqttAsyncClient getMqttAsyncClient(String clientId, String username, String password) throws MqttException { + if (StringUtils.isEmpty(clientId)) { + clientId = MqttAsyncClient.generateClientId(); + } + MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence()); + + MqttConnectOptions options = new MqttConnectOptions(); + if (StringUtils.isNotEmpty(username)) { + options.setUserName(username); + } + if (StringUtils.isNotEmpty(password)) { + options.setPassword(password.toCharArray()); + } + client.connect(options).waitForCompletion(); + return client; + } + + private Device createDevice(String deviceName, BasicMqttCredentials clientIdCredValue) throws Exception { + Device device = new Device(); + device.setName(deviceName); + device.setType("default"); + + device = doPost("/api/device", device, Device.class); + + DeviceCredentials clientIdCred = + doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + clientIdCred.setCredentialsType(DeviceCredentialsType.MQTT_BASIC); + + + clientIdCred.setCredentialsValue(JacksonUtil.toString(clientIdCredValue)); + doPost("/api/device/credentials", clientIdCred).andExpect(status().isOk()); + return device; + } + + private Device createDevice(String deviceName, String accessToken) throws Exception { + Device device = new Device(); + device.setName(deviceName); + device.setType("default"); + + device = doPost("/api/device", device, Device.class); + + DeviceCredentials clientIdCred = + doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class); + + clientIdCred.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + clientIdCred.setCredentialsId(accessToken); + doPost("/api/device/credentials", clientIdCred).andExpect(status().isOk()); + return device; + } +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java index 870b62b195..f2aef88273 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionJsonDeviceTest.java @@ -94,7 +94,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); @@ -103,7 +103,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -119,7 +119,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); String requestCredentials = ",\"credentialsType\": \"ACCESS_TOKEN\",\"token\": \"test_token\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -138,7 +138,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); String requestCredentials = ",\"credentialsType\": \"X509_CERTIFICATE\",\"hash\": \"testHash\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -163,7 +163,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); String requestCredentials = ",\"credentialsType\": \"MQTT_BASIC\",\"clientId\": \"test_clientId\",\"username\": \"test_username\",\"password\": \"test_password\""; byte[] result = createMqttClientAndPublish(requestCredentials).getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -188,7 +188,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, false, false); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); @@ -199,7 +199,7 @@ public abstract class AbstractMqttProvisionJsonDeviceTest extends AbstractMqttIn } protected void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.JSON, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, false, false); byte[] result = createMqttClientAndPublish().getPayloadBytes(); JsonObject response = JsonUtils.parse(new String(result)).getAsJsonObject(); Assert.assertEquals("Provision data was not found!", response.get("errorMsg").getAsString()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java index 97ebbfe4a7..bfa4f6badf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/AbstractMqttProvisionProtoDeviceTest.java @@ -101,14 +101,14 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI protected void processTestProvisioningDisabledDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); ProvisionDeviceResponseMsg result = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Assert.assertNotNull(result); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), result.getStatus().toString()); } protected void processTestProvisioningCreateNewDeviceWithoutCredentials() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Device createdDevice = deviceService.findDeviceByTenantIdAndName(savedTenant.getTenantId(), "Test Provision device"); @@ -122,7 +122,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithAccessToken() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null,null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null,null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceTokenRequestMsg(ValidateDeviceTokenRequestMsg.newBuilder().setToken("test_token").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.ACCESS_TOKEN, requestCredentials)).getPayloadBytes()); @@ -140,7 +140,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithCert() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateDeviceX509CertRequestMsg(ValidateDeviceX509CertRequestMsg.newBuilder().setHash("testHash").build()).build(); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish(createTestsProvisionMessage(CredentialsType.X509_CERTIFICATE, requestCredentials)).getPayloadBytes()); @@ -164,7 +164,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCreateNewDeviceWithMqttBasic() throws Exception { - super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES); + super.processBeforeTest("Test Provision device3", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.ALLOW_CREATE_NEW_DEVICES, false, false); CredentialsDataProto requestCredentials = CredentialsDataProto.newBuilder().setValidateBasicMqttCredRequestMsg( ValidateBasicMqttCredRequestMsg.newBuilder() .setClientId("test_clientId") @@ -195,7 +195,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningCheckPreProvisionedDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKey", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, false, false); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(savedTenant.getTenantId(), savedDevice.getId()); @@ -205,7 +205,7 @@ public abstract class AbstractMqttProvisionProtoDeviceTest extends AbstractMqttI } protected void processTestProvisioningWithBadKeyDevice() throws Exception { - super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES); + super.processBeforeTest("Test Provision device", "Test Provision gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, null, "testProvisionKeyOrig", "testProvisionSecret", DeviceProfileProvisionType.CHECK_PRE_PROVISIONED_DEVICES, false, false); ProvisionDeviceResponseMsg response = ProvisionDeviceResponseMsg.parseFrom(createMqttClientAndPublish().getPayloadBytes()); Assert.assertEquals(ProvisionResponseStatus.NOT_FOUND.name(), response.getStatus().toString()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceJsonTest.java similarity index 90% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceJsonTest.java index bca75c87b2..5cb9ae345a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceJsonSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceJsonTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.provision.AbstractMqttProvisionJsonDeviceTest; @DaoSqlTest -public class MqttProvisionDeviceJsonSqlTest extends AbstractMqttProvisionJsonDeviceTest { +public class MqttProvisionDeviceJsonTest extends AbstractMqttProvisionJsonDeviceTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceProtoTest.java similarity index 90% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceProtoTest.java index 62cb3d4779..1e32fab350 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceProtoSqlTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/provision/sql/MqttProvisionDeviceProtoTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.provision.AbstractMqttProvisionProtoDeviceTest; @DaoSqlTest -public class MqttProvisionDeviceProtoSqlTest extends AbstractMqttProvisionProtoDeviceTest { +public class MqttProvisionDeviceProtoTest extends AbstractMqttProvisionProtoDeviceTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcBackwardCompatibilityIntegrationTest.java new file mode 100644 index 0000000000..f8b3949b60 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcBackwardCompatibilityIntegrationTest.java @@ -0,0 +1,94 @@ +/** + * 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.transport.mqtt.rpc; + +import lombok.extern.slf4j.Slf4j; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.DeviceProfileProvisionType; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.device.profile.MqttTopics; + +@Slf4j +public abstract class AbstractMqttServerSideRpcBackwardCompatibilityIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { + + @After + public void afterTest() throws Exception { + super.processAfterTest(); + } + + @Test + public void testServerMqttOneWayRpcWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttOneWayRpcOnShortProtoTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processOneWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcWithEnabledJsonCompatibility() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, false); + processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortProtoTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + } + + @Test + public void testServerMqttTwoWayRpcOnShortJsonTopicWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + } + + @Test + public void testGatewayServerMqttOneWayRpcWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processProtoOneWayRpcTestGateway("Gateway Device OneWay RPC Proto"); + } + + @Test + public void testGatewayServerMqttTwoWayRpcWithEnabledJsonCompatibilityAndJsonDownlinks() throws Exception { + super.processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, true, true); + processProtoTwoWayRpcTestGateway("Gateway Device TwoWay RPC Proto"); + } + +} 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 9be3d1d2ca..fbb66e61c8 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 @@ -97,17 +97,17 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab @Test public void testServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortJsonTopic() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test @@ -117,12 +117,12 @@ public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends Ab @Test public void testGatewayServerMqttOneWayRpc() throws Exception { - processOneWayRpcTestGateway("Gateway Device OneWay RPC"); + processJsonOneWayRpcTestGateway("Gateway Device OneWay RPC"); } @Test public void testGatewayServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTestGateway("Gateway Device TwoWay RPC"); + processJsonTwoWayRpcTestGateway("Gateway Device TwoWay RPC"); } } 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 1b468194b6..2d6a653264 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 @@ -17,8 +17,12 @@ package org.thingsboard.server.transport.mqtt.rpc; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.os72.protobuf.dynamic.DynamicSchema; +import com.google.protobuf.Descriptors; +import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.nimbusds.jose.util.StandardCharset; +import com.squareup.wire.schema.internal.parser.ProtoFileElement; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -31,18 +35,23 @@ import org.junit.Assert; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; 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; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -51,7 +60,21 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractMqttIntegrationTest { - protected static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; + protected static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + "package rpc;\n" + + "\n" + + "message RpcRequestMsg {\n" + + " optional string method = 1;\n" + + " optional int32 requestId = 2;\n" + + " Params params = 3;\n" + + "\n" + + " message Params {\n" + + " optional string pin = 1;\n" + + " optional int32 value = 2;\n" + + " }\n" + + "}"; + + private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; protected Long asyncContextTimeoutToUseRpcPlugin; @@ -64,7 +87,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttAsyncClient client = getMqttAsyncClient(accessToken); CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); + TestOneWayMqttCallback callback = new TestOneWayMqttCallback(client, latch); client.setCallback(callback); client.subscribe(rpcSubTopic, MqttQoS.AT_MOST_ONCE.value()); @@ -79,19 +102,19 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + protected void processJsonOneWayRpcTestGateway(String deviceName) throws Exception { MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); String payload = "{\"device\":\"" + deviceName + "\"}"; byte[] payloadBytes = payload.getBytes(); validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); } - protected void processTwoWayRpcTest(String rpcSubTopic) throws Exception { + protected void processJsonTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttAsyncClient client = getMqttAsyncClient(accessToken); client.subscribe(rpcSubTopic, 1); CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); + TestJsonMqttCallback callback = new TestJsonMqttCallback(client, latch); client.setCallback(callback); Thread.sleep(1000); @@ -105,6 +128,46 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM Assert.assertEquals(expected, result); } + protected void processProtoTwoWayRpcTest(String rpcSubTopic) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(accessToken); + client.subscribe(rpcSubTopic, 1); + + CountDownLatch latch = new CountDownLatch(1); + TestProtoMqttCallback callback = new TestProtoMqttCallback(client, latch); + client.setCallback(callback); + + Thread.sleep(1000); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + + String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; + latch.await(3, TimeUnit.SECONDS); + Assert.assertEquals(expected, result); + } + + protected void processProtoTwoWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + byte[] payloadBytes = connectMsgProto.toByteArray(); + validateProtoTwoWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + + protected void processProtoOneWayRpcTestGateway(String deviceName) throws Exception { + MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); + TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); + byte[] payloadBytes = connectMsgProto.toByteArray(); + validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); + } + + private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { + TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); + builder.setDeviceName(deviceName); + builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); + return builder.build(); + } + protected void processSequenceTwoWayRpcTest() throws Exception { List expected = new ArrayList<>(); List result = new ArrayList<>(); @@ -131,13 +194,13 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM Assert.assertEquals(expected, result); } - protected void processTwoWayRpcTestGateway(String deviceName) throws Exception { + protected void processJsonTwoWayRpcTestGateway(String deviceName) throws Exception { MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); String payload = "{\"device\":\"" + deviceName + "\"}"; byte[] payloadBytes = payload.getBytes(); - validateTwoWayRpcGateway(deviceName, client, payloadBytes); + validateJsonTwoWayRpcGatewayResponse(deviceName, client, payloadBytes); } protected void validateOneWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { @@ -151,7 +214,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM assertNotNull(savedDevice); CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); + TestOneWayMqttCallback callback = new TestOneWayMqttCallback(client, latch); client.setCallback(callback); client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); @@ -166,7 +229,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); } - protected void validateTwoWayRpcGateway(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { + protected void validateJsonTwoWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); Device savedDevice = doExecuteWithRetriesAndInterval( @@ -177,7 +240,34 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM assertNotNull(savedDevice); CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); + TestJsonMqttCallback callback = new TestJsonMqttCallback(client, latch); + client.setCallback(callback); + + client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); + + Thread.sleep(1000); + + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; + String deviceId = savedDevice.getId().getId().toString(); + String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + latch.await(3, TimeUnit.SECONDS); + String expected = "{\"success\":true}"; + assertEquals(expected, result); + assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS()); + } + + protected void validateProtoTwoWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception { + publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC); + + Device savedDevice = doExecuteWithRetriesAndInterval( + () -> getDeviceByName(deviceName), + 20, + 100 + ); + assertNotNull(savedDevice); + + CountDownLatch latch = new CountDownLatch(1); + TestProtoMqttCallback callback = new TestProtoMqttCallback(client, latch); client.setCallback(callback); client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value()); @@ -197,7 +287,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM return doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class); } - protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + protected MqttMessage processJsonMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { MqttMessage message = new MqttMessage(); if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC) || requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC_V2)) { message.setPayload(DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8)); @@ -210,13 +300,45 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM return message; } - protected class TestMqttCallback implements MqttCallback { + protected class TestOneWayMqttCallback implements MqttCallback { + + private final MqttAsyncClient client; + private final CountDownLatch latch; + private Integer qoS; + + TestOneWayMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + this.client = client; + this.latch = latch; + } + + int getQoS() { + return qoS; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); + qoS = mqttMessage.getQos(); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + protected class TestJsonMqttCallback implements MqttCallback { private final MqttAsyncClient client; private final CountDownLatch latch; private Integer qoS; - TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + TestJsonMqttCallback(MqttAsyncClient client, CountDownLatch latch) { this.client = client; this.latch = latch; } @@ -239,7 +361,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM responseTopic = requestTopic.replace("request", "response"); } qoS = mqttMessage.getQos(); - client.publish(responseTopic, processMessageArrived(requestTopic, mqttMessage)); + client.publish(responseTopic, processJsonMessageArrived(requestTopic, mqttMessage)); latch.countDown(); } @@ -249,6 +371,98 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM } } + protected class TestProtoMqttCallback implements MqttCallback { + + private final MqttAsyncClient client; + private final CountDownLatch latch; + private Integer qoS; + + TestProtoMqttCallback(MqttAsyncClient client, CountDownLatch latch) { + this.client = client; + this.latch = latch; + } + + int getQoS() { + return qoS; + } + + @Override + public void connectionLost(Throwable throwable) { + } + + @Override + public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception { + log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload())); + 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, processProtoMessageArrived(requestTopic, mqttMessage)); + latch.countDown(); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + } + + protected MqttMessage processProtoMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { + MqttMessage message = new MqttMessage(); + 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); + + byte[] requestPayload = mqttMessage.getPayload(); + DynamicMessage.Builder rpcRequestMsg = rpcRequestProtoSchema.newMessageBuilder("RpcRequestMsg"); + Descriptors.Descriptor rpcRequestMsgDescriptor = rpcRequestMsg.getDescriptorForType(); + assertNotNull(rpcRequestMsgDescriptor); + try { + DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload); + List fields = rpcRequestMsgDescriptor.getFields(); + for (Descriptors.FieldDescriptor fieldDescriptor: fields) { + assertTrue(dynamicMessage.hasField(fieldDescriptor)); + } + ProtoFileElement transportProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_RPC_RESPONSE_PROTO_SCHEMA); + DynamicSchema rpcResponseProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_RESPONSE_PROTO_SCHEMA); + + DynamicMessage.Builder rpcResponseBuilder = rpcResponseProtoSchema.newMessageBuilder("RpcResponseMsg"); + Descriptors.Descriptor rpcResponseMsgDescriptor = rpcResponseBuilder.getDescriptorForType(); + assertNotNull(rpcResponseMsgDescriptor); + DynamicMessage rpcResponseMsg = rpcResponseBuilder + .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) + .build(); + message.setPayload(rpcResponseMsg.toByteArray()); + } catch (InvalidProtocolBufferException e) { + log.warn("Command Response Ack Error, Invalid response received: ", e); + } + } else { + TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); + String deviceName = msg.getDeviceName(); + int requestId = msg.getRpcRequestMsg().getRequestId(); + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.newBuilder() + .setDeviceName(deviceName) + .setId(requestId) + .setData("{\"success\": true}") + .build(); + message.setPayload(gatewayRpcResponseMsg.toByteArray()); + } + return message; + } + + private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { + DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); + assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); + MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; + TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); + assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); + return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + } + protected class TestSequenceMqttCallback implements MqttCallback { private final MqttAsyncClient client; @@ -273,7 +487,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM var qoS = mqttMessage.getQos(); client.messageArrivedComplete(mqttMessage.getId(), qoS); - client.publish(responseTopic, processMessageArrived(requestTopic, mqttMessage)); + client.publish(responseTopic, processJsonMessageArrived(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 f8c0cd8bed..14098c6e2a 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 @@ -53,30 +53,30 @@ public abstract class AbstractMqttServerSideRpcJsonIntegrationTest extends Abstr @Test public void testServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortJsonTopic() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); + processJsonTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC); } @Test public void testGatewayServerMqttOneWayRpc() throws Exception { - processOneWayRpcTestGateway("Gateway Device OneWay RPC Json"); + processJsonOneWayRpcTestGateway("Gateway Device OneWay RPC Json"); } @Test public void testGatewayServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Json"); + processJsonTwoWayRpcTestGateway("Gateway Device TwoWay RPC Json"); } - protected void processOneWayRpcTestGateway(String deviceName) throws Exception { + protected void processJsonOneWayRpcTestGateway(String deviceName) throws Exception { MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); String payload = "{\"device\": \"" + deviceName + "\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}"; byte[] payloadBytes = payload.getBytes(); 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 2c9f1f7889..30227dfe3b 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 @@ -15,60 +15,20 @@ */ package org.thingsboard.server.transport.mqtt.rpc; -import com.github.os72.protobuf.dynamic.DynamicSchema; -import com.google.protobuf.Descriptors; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.InvalidProtocolBufferException; -import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.core.CoapHandler; -import org.eclipse.californium.core.CoapResponse; -import org.eclipse.californium.core.coap.MediaTypeRegistry; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.jetbrains.annotations.NotNull; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.DeviceProfileProvisionType; 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; - -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @Slf4j public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest { - private static final String RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + - "package rpc;\n" + - "\n" + - "message RpcRequestMsg {\n" + - " optional string method = 1;\n" + - " optional int32 requestId = 2;\n" + - " Params params = 3;\n" + - "\n" + - " message Params {\n" + - " optional string pin = 1;\n" + - " optional int32 value = 2;\n" + - " }\n" + - "}"; - @Before public void beforeTest() throws Exception { - processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED); + processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null, null, null, null, RPC_REQUEST_PROTO_SCHEMA, null, null, DeviceProfileProvisionType.DISABLED, false, false); } @After @@ -93,122 +53,27 @@ public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends Abst @Test public void testServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); + processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortTopic() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); + processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC); } @Test public void testServerMqttTwoWayRpcOnShortProtoTopic() throws Exception { - processTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); + processProtoTwoWayRpcTest(MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC); } @Test public void testGatewayServerMqttOneWayRpc() throws Exception { - processOneWayRpcTestGateway("Gateway Device OneWay RPC Proto"); + processProtoOneWayRpcTestGateway("Gateway Device OneWay RPC Proto"); } @Test public void testGatewayServerMqttTwoWayRpc() throws Exception { - processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Proto"); - } - - protected void processTwoWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); - byte[] payloadBytes = connectMsgProto.toByteArray(); - validateTwoWayRpcGateway(deviceName, client, payloadBytes); - } - - protected void processOneWayRpcTestGateway(String deviceName) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); - TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); - byte[] payloadBytes = connectMsgProto.toByteArray(); - validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes); - } - - - private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) { - TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder(); - builder.setDeviceName(deviceName); - builder.setDeviceType(TransportPayloadType.PROTOBUF.name()); - return builder.build(); + processProtoTwoWayRpcTestGateway("Gateway Device TwoWay RPC Proto"); } - protected void processTwoWayRpcTest(String rpcSubTopic) throws Exception { - MqttAsyncClient client = getMqttAsyncClient(accessToken); - client.subscribe(rpcSubTopic, 1); - - CountDownLatch latch = new CountDownLatch(1); - TestMqttCallback callback = new TestMqttCallback(client, latch); - client.setCallback(callback); - - Thread.sleep(1000); - - String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; - String deviceId = savedDevice.getId().getId().toString(); - - String result = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); - String expected = "{\"payload\":\"{\\\"value1\\\":\\\"A\\\",\\\"value2\\\":\\\"B\\\"}\"}"; - latch.await(3, TimeUnit.SECONDS); - Assert.assertEquals(expected, result); - } - - protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException { - MqttMessage message = new MqttMessage(); - 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); - - byte[] requestPayload = mqttMessage.getPayload(); - DynamicMessage.Builder rpcRequestMsg = rpcRequestProtoSchema.newMessageBuilder("RpcRequestMsg"); - Descriptors.Descriptor rpcRequestMsgDescriptor = rpcRequestMsg.getDescriptorForType(); - assertNotNull(rpcRequestMsgDescriptor); - try { - DynamicMessage dynamicMessage = DynamicMessage.parseFrom(rpcRequestMsgDescriptor, requestPayload); - List fields = rpcRequestMsgDescriptor.getFields(); - for (Descriptors.FieldDescriptor fieldDescriptor: fields) { - assertTrue(dynamicMessage.hasField(fieldDescriptor)); - } - ProtoFileElement transportProtoSchemaFile = protoTransportPayloadConfiguration.getTransportProtoSchema(DEVICE_RPC_RESPONSE_PROTO_SCHEMA); - DynamicSchema rpcResponseProtoSchema = protoTransportPayloadConfiguration.getDynamicSchema(transportProtoSchemaFile, ProtoTransportPayloadConfiguration.RPC_RESPONSE_PROTO_SCHEMA); - - DynamicMessage.Builder rpcResponseBuilder = rpcResponseProtoSchema.newMessageBuilder("RpcResponseMsg"); - Descriptors.Descriptor rpcResponseMsgDescriptor = rpcResponseBuilder.getDescriptorForType(); - assertNotNull(rpcResponseMsgDescriptor); - DynamicMessage rpcResponseMsg = rpcResponseBuilder - .setField(rpcResponseMsgDescriptor.findFieldByName("payload"), DEVICE_RESPONSE) - .build(); - message.setPayload(rpcResponseMsg.toByteArray()); - } catch (InvalidProtocolBufferException e) { - log.warn("Command Response Ack Error, Invalid response received: ", e); - } - } else { - TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload()); - String deviceName = msg.getDeviceName(); - int requestId = msg.getRpcRequestMsg().getRequestId(); - TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.newBuilder() - .setDeviceName(deviceName) - .setId(requestId) - .setData("{\"success\": true}") - .build(); - message.setPayload(gatewayRpcResponseMsg.toByteArray()); - } - return message; - } - - private ProtoTransportPayloadConfiguration getProtoTransportPayloadConfiguration() { - DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); - assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); - MqttDeviceProfileTransportConfiguration mqttTransportConfiguration = (MqttDeviceProfileTransportConfiguration) transportConfiguration; - TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttTransportConfiguration.getTransportPayloadTypeConfiguration(); - assertTrue(transportPayloadTypeConfiguration instanceof ProtoTransportPayloadConfiguration); - return (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; - } - - } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java new file mode 100644 index 0000000000..559ae945d1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcBackwardCompatibilityIntegrationTest.java @@ -0,0 +1,24 @@ +/** + * 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.transport.mqtt.rpc.sql; + +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.mqtt.rpc.AbstractMqttServerSideRpcBackwardCompatibilityIntegrationTest; + + +@DaoSqlTest +public class MqttServerSideRpcBackwardCompatibilityIntegrationTest extends AbstractMqttServerSideRpcBackwardCompatibilityIntegrationTest { +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcIntegrationTest.java similarity index 89% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcIntegrationTest.java index 816ac7780d..b0438ca96b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcIntegrationTest.java @@ -22,5 +22,5 @@ import org.thingsboard.server.transport.mqtt.rpc.AbstractMqttServerSideRpcDefaul * Created by Valerii Sosliuk on 8/22/2017. */ @DaoSqlTest -public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest { +public class MqttServerSideRpcIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcJsonIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcJsonIntegrationTest.java index c17570b5ca..22eeea1624 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcJsonIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.rpc.AbstractMqttServerSideRpcJsonIntegrationTest; @DaoSqlTest -public class MqttServerSideRpcJsonSqlIntegrationTest extends AbstractMqttServerSideRpcJsonIntegrationTest { +public class MqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSideRpcJsonIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcProtoIntegrationTest.java similarity index 88% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcProtoIntegrationTest.java index fa21510bb1..0daf14b7c3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/sql/MqttServerSideRpcProtoIntegrationTest.java @@ -20,5 +20,5 @@ import org.thingsboard.server.transport.mqtt.rpc.AbstractMqttServerSideRpcProtoI @DaoSqlTest -public class MqttServerSideRpcProtoSqlIntegrationTest extends AbstractMqttServerSideRpcProtoIntegrationTest { +public class MqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcProtoIntegrationTest { } 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 afe624c55b..a68ece0df0 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 @@ -54,6 +54,12 @@ public abstract class AbstractMqttAttributesProtoIntegrationTest extends Abstrac processAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postAttributesMsg.toByteArray(), false); } + @Test + public void testPushAttributesWithEnabledJsonBackwardCompatibility() throws Exception { + super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC, true, false); + processJsonPayloadAttributesTest(POST_DATA_ATTRIBUTES_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes()); + } + @Test public void testPushAttributesWithExplicitPresenceProtoKeys() throws Exception { super.processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", TransportPayloadType.PROTOBUF, null, POST_DATA_ATTRIBUTES_TOPIC); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesIntegrationTest.java similarity index 90% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesIntegrationTest.java index c3dafc4eef..06f222f8a1 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.telemetry.attributes.AbstractMqttAttributesIntegrationTest; @DaoSqlTest -public class MqttAttributesSqlIntegrationTest extends AbstractMqttAttributesIntegrationTest { +public class MqttAttributesIntegrationTest extends AbstractMqttAttributesIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesJsonIntegrationTest.java similarity index 89% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesJsonIntegrationTest.java index 306b89ad23..eefde6336c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesJsonIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.telemetry.attributes.AbstractMqttAttributesJsonIntegrationTest; @DaoSqlTest -public class MqttAttributesSqlJsonIntegrationTest extends AbstractMqttAttributesJsonIntegrationTest { +public class MqttAttributesJsonIntegrationTest extends AbstractMqttAttributesJsonIntegrationTest { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesProtoIntegrationTest.java similarity index 89% rename from application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java rename to application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesProtoIntegrationTest.java index 57376961db..a976f54e35 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesSqlProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/telemetry/attributes/sql/MqttAttributesProtoIntegrationTest.java @@ -19,5 +19,5 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.mqtt.telemetry.attributes.AbstractMqttAttributesProtoIntegrationTest; @DaoSqlTest -public class MqttAttributesSqlProtoIntegrationTest extends AbstractMqttAttributesProtoIntegrationTest { +public class MqttAttributesProtoIntegrationTest extends AbstractMqttAttributesProtoIntegrationTest { } 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 a10e19a35c..fd6cf9b9ae 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,9 +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; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @@ -37,7 +35,6 @@ import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.Arrays; -import java.util.Collections; import java.util.List; import static org.junit.Assert.assertNotNull; @@ -60,6 +57,12 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac processTelemetryTest(POST_DATA_TELEMETRY_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), postTelemetryMsg.toByteArray(), false, false); } + @Test + public void testPushTelemetryWithEnabledJsonBackwardCompatibility() throws Exception { + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, true, false); + processJsonPayloadTelemetryTest(POST_DATA_TELEMETRY_TOPIC, Arrays.asList("key1", "key2", "key3", "key4", "key5"), PAYLOAD_VALUES_STR.getBytes(), false); + } + @Test public void testPushTelemetryWithTs() throws Exception { String schemaStr = "syntax =\"proto3\";\n" + @@ -87,7 +90,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac " }\n" + " }\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); + 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, false, false); DynamicSchema telemetrySchema = getDynamicSchema(schemaStr); DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); @@ -189,7 +192,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac " }\n" + " }\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); + 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, false, false); DynamicSchema telemetrySchema = getDynamicSchema(schemaStr); DynamicMessage.Builder nestedJsonObjectBuilder = telemetrySchema.newMessageBuilder("PostTelemetry.JsonObject.NestedJsonObject"); @@ -249,7 +252,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @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); + 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, false, false); TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); String deviceName1 = "Device A"; @@ -263,7 +266,7 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac @Test public void testGatewayConnect() throws Exception { - super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED); + super.processBeforeTest("Test Post Telemetry device proto payload", "Test Post Telemetry gateway proto payload", TransportPayloadType.PROTOBUF, POST_DATA_TELEMETRY_TOPIC, null, null, null, null, null, null, null, DeviceProfileProvisionType.DISABLED, false, false); String deviceName = "Device A"; TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName); MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken); diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index f65bce749e..6b2e8d154b 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -1,10 +1,21 @@ -transport.lwm2m.security.key_store=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.security.key_store_password=server +transport.lwm2m.server.security.credentials.enabled=true +transport.lwm2m.server.security.credentials.type=KEYSTORE +transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.server.security.credentials.keystore.store_password=server +transport.lwm2m.server.security.credentials.keystore.key_alias=server +transport.lwm2m.server.security.credentials.keystore.key_password=server +transport.lwm2m.bootstrap.security.credentials.enabled=true +transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE +transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server +transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server +transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server +transport.lwm2m.security.trust-credentials.enabled=true +transport.lwm2m.security.trust-credentials.type=KEYSTORE +transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.security.trust-credentials.keystore.store_password=server + edges.enabled=true edges.storage.no_read_records_sleep=500 edges.storage.sleep_between_batches=500 -transport.lwm2m.server.security.key_alias=server -transport.lwm2m.server.security.key_password=server -transport.lwm2m.bootstrap.security.key_alias=server -transport.lwm2m.bootstrap.security.key_password=server -actors.rpc.sequential=true \ No newline at end of file +actors.rpc.sequential=true diff --git a/application/src/test/resources/update/330/README.md b/application/src/test/resources/update/330/README.md new file mode 100644 index 0000000000..26c5e74898 --- /dev/null +++ b/application/src/test/resources/update/330/README.md @@ -0,0 +1,3 @@ +To get json from live Thingsboard instance use those methods: +1. Browser: F12 -> Network -> open device profile page -> copy raw response +2. Database: SELECT * FROM public.device_profile WHERE name = 'LORAWAN 001'; \ No newline at end of file diff --git a/application/src/test/resources/update/330/device_profile_001_in.json b/application/src/test/resources/update/330/device_profile_001_in.json new file mode 100644 index 0000000000..16d1f09e43 --- /dev/null +++ b/application/src/test/resources/update/330/device_profile_001_in.json @@ -0,0 +1,173 @@ +{ + "id": { + "entityType": "DEVICE_PROFILE", + "id": "b99fde7a-33dd-4d5d-a325-d0637f6acbe5" + }, + "createdTime": 1627268171906, + "tenantId": { + "entityType": "TENANT", + "id": "3db30ac6-db03-4788-98fe-6e024b422a15" + }, + "name": "LORAWAN 001", + "description": "Tektelic - 001", + "type": "DEFAULT", + "transportType": "DEFAULT", + "provisionType": "DISABLED", + "defaultRuleChainId": { + "entityType": "RULE_CHAIN", + "id": "9c50f4df-f41e-443f-bb7d-37b5ac97f3c3" + }, + "defaultQueueName": "LORAWAN", + "profileData": { + "configuration": { + "type": "DEFAULT" + }, + "transportConfiguration": { + "type": "DEFAULT" + }, + "provisionConfiguration": { + "type": "DISABLED", + "provisionDeviceSecret": null + }, + "alarms": [ + { + "id": "b86271fd-5fee-4bd5-975c-d9c18f610cd5", + "alarmType": "LORAWAN - Battery Alarm", + "createRules": { + "CRITICAL": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "batteryLevel" + }, + "valueType": "NUMERIC", + "value": null, + "predicate": { + "type": "NUMERIC", + "operation": "LESS", + "value": { + "defaultValue": 25.0, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "DURATION", + "unit": "DAYS", + "value": 1 + } + }, + "schedule": null, + "alarmDetails": null + } + }, + "clearRule": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "batteryLevel" + }, + "valueType": "NUMERIC", + "value": null, + "predicate": { + "type": "NUMERIC", + "operation": "GREATER_OR_EQUAL", + "value": { + "defaultValue": 25.0, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "DURATION", + "unit": "DAYS", + "value": 1 + } + }, + "schedule": null, + "alarmDetails": null + }, + "propagate": true, + "propagateRelationTypes": [ + "UC-0007 LORAWAN" + ] + }, + { + "id": "c70aef4e-65cf-4578-acd9-e1927c08b469", + "alarmType": "LORAWAN - No Data", + "createRules": { + "CRITICAL": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "active" + }, + "valueType": "BOOLEAN", + "value": null, + "predicate": { + "type": "BOOLEAN", + "operation": "EQUAL", + "value": { + "defaultValue": false, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "SIMPLE" + } + }, + "schedule": null, + "alarmDetails": null + } + }, + "clearRule": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "active" + }, + "valueType": "BOOLEAN", + "value": null, + "predicate": { + "type": "BOOLEAN", + "operation": "EQUAL", + "value": { + "defaultValue": true, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "SIMPLE" + } + }, + "schedule": null, + "alarmDetails": null + }, + "propagate": true, + "propagateRelationTypes": [ + "LORAWAN 001 related" + ] + } + ] + }, + "provisionDeviceKey": null, + "default": false +} diff --git a/application/src/test/resources/update/330/device_profile_001_out.json b/application/src/test/resources/update/330/device_profile_001_out.json new file mode 100644 index 0000000000..9a349c6638 --- /dev/null +++ b/application/src/test/resources/update/330/device_profile_001_out.json @@ -0,0 +1,189 @@ +{ + "id": { + "entityType": "DEVICE_PROFILE", + "id": "b99fde7a-33dd-4d5d-a325-d0637f6acbe5" + }, + "createdTime": 1627268171906, + "tenantId": { + "entityType": "TENANT", + "id": "3db30ac6-db03-4788-98fe-6e024b422a15" + }, + "name": "LORAWAN 001", + "description": "Tektelic - 001", + "type": "DEFAULT", + "transportType": "DEFAULT", + "provisionType": "DISABLED", + "defaultRuleChainId": { + "entityType": "RULE_CHAIN", + "id": "9c50f4df-f41e-443f-bb7d-37b5ac97f3c3" + }, + "defaultQueueName": "LORAWAN", + "profileData": { + "configuration": { + "type": "DEFAULT" + }, + "transportConfiguration": { + "type": "DEFAULT" + }, + "provisionConfiguration": { + "type": "DISABLED", + "provisionDeviceSecret": null + }, + "alarms": [ + { + "id": "b86271fd-5fee-4bd5-975c-d9c18f610cd5", + "alarmType": "LORAWAN - Battery Alarm", + "createRules": { + "CRITICAL": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "batteryLevel" + }, + "valueType": "NUMERIC", + "value": null, + "predicate": { + "type": "NUMERIC", + "operation": "LESS", + "value": { + "defaultValue": 25.0, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "DURATION", + "unit": "DAYS", + "predicate": { + "defaultValue": 1, + "userValue": null, + "dynamicValue": { + "sourceType": null, + "sourceAttribute": null, + "inherit": false + } + } + } + }, + "schedule": null, + "alarmDetails": null + } + }, + "clearRule": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "batteryLevel" + }, + "valueType": "NUMERIC", + "value": null, + "predicate": { + "type": "NUMERIC", + "operation": "GREATER_OR_EQUAL", + "value": { + "defaultValue": 25.0, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "DURATION", + "unit": "DAYS", + "predicate": { + "defaultValue": 1, + "userValue": null, + "dynamicValue": { + "sourceType": null, + "sourceAttribute": null, + "inherit": false + } + } + } + }, + "schedule": null, + "alarmDetails": null + }, + "propagate": true, + "propagateRelationTypes": [ + "UC-0007 LORAWAN" + ] + }, + { + "id": "c70aef4e-65cf-4578-acd9-e1927c08b469", + "alarmType": "LORAWAN - No Data", + "createRules": { + "CRITICAL": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "active" + }, + "valueType": "BOOLEAN", + "value": null, + "predicate": { + "type": "BOOLEAN", + "operation": "EQUAL", + "value": { + "defaultValue": false, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "SIMPLE" + } + }, + "schedule": null, + "alarmDetails": null + } + }, + "clearRule": { + "condition": { + "condition": [ + { + "key": { + "type": "TIME_SERIES", + "key": "active" + }, + "valueType": "BOOLEAN", + "value": null, + "predicate": { + "type": "BOOLEAN", + "operation": "EQUAL", + "value": { + "defaultValue": true, + "userValue": null, + "dynamicValue": null + } + } + } + ], + "spec": { + "type": "SIMPLE" + } + }, + "schedule": null, + "alarmDetails": null + }, + "propagate": true, + "propagateRelationTypes": [ + "LORAWAN 001 related" + ] + } + ] + }, + "provisionDeviceKey": null, + "default": false +} diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 90e7bf22d0..16bd96eedb 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 1637621273..f5bf91c9f0 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 4c59e022d4..6a337c414d 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 5b220974ef..dca92c1c7a 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -14,6 +14,7 @@ * limitations under the License. */ syntax = "proto3"; + package transport; option java_package = "org.thingsboard.server.gen.transport"; @@ -581,8 +582,12 @@ message TbSubscriptionKetStateProto { message TbSubscriptionUpdateValueListProto { string key = 1; - repeated int64 ts = 2; - repeated string value = 3; + repeated TbSubscriptionUpdateTsValue tsValue = 2; +} + +message TbSubscriptionUpdateTsValue { + int64 ts = 1; + optional string value = 2; } /** diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 945556c188..a8762a81d2 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java index f433909242..6f82c79041 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java @@ -20,11 +20,16 @@ import org.eclipse.californium.elements.util.SslContextUtil; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.dtls.CertificateType; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import java.io.IOException; @@ -45,17 +50,15 @@ public class TbCoapDtlsSettings { @Value("${transport.coap.dtls.bind_port}") private Integer port; - @Value("${transport.coap.dtls.key_store}") - private String keyStoreFile; - - @Value("${transport.coap.dtls.key_store_password}") - private String keyStorePassword; - - @Value("${transport.coap.dtls.key_password}") - private String keyPassword; + @Bean + @ConfigurationProperties(prefix = "transport.coap.dtls.credentials") + public SslCredentialsConfig coapDtlsCredentials() { + return new SslCredentialsConfig("COAP DTLS Credentials", false); + } - @Value("${transport.coap.dtls.key_alias}") - private String keyAlias; + @Autowired + @Qualifier("coapDtlsCredentials") + private SslCredentialsConfig coapDtlsCredentialsConfig; @Value("${transport.coap.dtls.x509.skip_validity_check_for_client_cert:false}") private boolean skipValidityCheckForClientCert; @@ -75,8 +78,9 @@ public class TbCoapDtlsSettings { public DtlsConnectorConfig dtlsConnectorConfig() throws UnknownHostException { DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder(); configBuilder.setAddress(getInetSocketAddress()); - String keyStoreFilePath = ResourceUtils.getUri(this, keyStoreFile); - SslContextUtil.Credentials serverCredentials = loadServerCredentials(keyStoreFilePath); + SslCredentials sslCredentials = this.coapDtlsCredentialsConfig.getCredentials(); + SslContextUtil.Credentials serverCredentials = + new SslContextUtil.Credentials(sslCredentials.getPrivateKey(), null, sslCredentials.getCertificateChain()); configBuilder.setServerOnly(true); configBuilder.setClientAuthenticationRequired(false); configBuilder.setClientAuthenticationWanted(true); @@ -94,15 +98,6 @@ public class TbCoapDtlsSettings { return configBuilder.build(); } - private SslContextUtil.Credentials loadServerCredentials(String keyStoreFilePath) { - try { - return SslContextUtil.loadCredentials(keyStoreFilePath, keyAlias, keyStorePassword.toCharArray(), - keyPassword.toCharArray()); - } catch (GeneralSecurityException | IOException e) { - throw new RuntimeException("Failed to load serverCredentials due to: ", e); - } - } - private InetSocketAddress getInetSocketAddress() throws UnknownHostException { InetAddress addr = InetAddress.getByName(host); return new InetSocketAddress(addr, port); diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index b5d26b8781..f861d2fb63 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index c0cddd9337..c175503d19 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -63,4 +63,6 @@ public interface AlarmService { PageData findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection orderedEntityIds); + + void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java index 29572bf51a..9afc735213 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java @@ -19,6 +19,8 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; +import java.util.List; + public interface DeviceCredentialsService { DeviceCredentials findDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId); @@ -32,4 +34,5 @@ public interface DeviceCredentialsService { void formatCredentials(DeviceCredentials deviceCredentials); void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java index 8e74af907b..9fa6c4d55f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/claim/ClaimData.java @@ -24,6 +24,8 @@ import java.io.Serializable; @Data public class ClaimData implements Serializable { + private static final long serialVersionUID = -3922621193389915930L; + private final String secretKey; private final long expirationTime; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index e6a9538e54..52fed96493 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -30,8 +30,6 @@ public interface EntityService { CustomerId fetchEntityCustomerId(TenantId tenantId, EntityId entityId); - void deleteEntityRelations(TenantId tenantId, EntityId entityId); - long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index c8f2a38005..87ac26b41e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -16,12 +16,16 @@ package org.thingsboard.server.dao.relation; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; @@ -78,6 +82,8 @@ public interface RelationService { void removeRelations(TenantId tenantId, EntityId entityId); + List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); 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..48f16215bb 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,13 +23,14 @@ 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; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; import java.util.List; @@ -43,7 +44,7 @@ public interface RuleChainService { boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId); - boolean saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData); + RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData); RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId); @@ -71,7 +72,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); @@ -89,4 +90,7 @@ public interface RuleChainService { PageData findAutoAssignToEdgeRuleChainsByTenantId(TenantId tenantId, PageLink pageLink); + List findRuleNodesByTenantIdAndType(TenantId tenantId, String name, String toString); + + RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode); } diff --git a/common/data/pom.xml b/common/data/pom.xml index 1b5c78bcd6..80be41e6a0 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common 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 61328dd352..041318d43c 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 @@ -21,6 +21,7 @@ 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.Length; import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @@ -29,6 +30,7 @@ public class AdminSettings extends BaseData { private static final long serialVersionUID = -7670322981725511892L; @NoXss + @Length(fieldName = "key") private String key; private transient JsonNode jsonValue; @@ -58,7 +60,7 @@ public class AdminSettings extends BaseData { return super.getCreatedTime(); } - @ApiModelProperty(position = 3, value = "The Administration Settings key, (e.g. 'general' or 'mail')") + @ApiModelProperty(position = 3, value = "The Administration Settings key, (e.g. 'general' or 'mail')", example = "mail") public String getKey() { return key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java index a333591e53..bcbce04898 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java @@ -17,30 +17,37 @@ package org.thingsboard.server.common.data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasName { - + private static final long serialVersionUID = 5047448057830660988L; + @Length(fieldName = "country") @NoXss protected String country; + @Length(fieldName = "state") @NoXss protected String state; + @Length(fieldName = "city") @NoXss protected String city; @NoXss protected String address; @NoXss protected String address2; + @Length(fieldName = "zip or postal code") @NoXss protected String zip; + @Length(fieldName = "phone") @NoXss protected String phone; + @Length(fieldName = "email") @NoXss protected String email; - + public ContactBased() { super(); } @@ -48,7 +55,7 @@ public abstract class ContactBased extends SearchTextBasedW public ContactBased(I id) { super(id); } - + public ContactBased(ContactBased contact) { super(contact); this.country = contact.getCountry(); 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..170731ca42 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,16 +18,22 @@ 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.Length; import org.thingsboard.server.common.data.validation.NoXss; public class Customer extends ContactBased implements HasTenantId { - + private static final long serialVersionUID = -1599722990298929275L; @NoXss + @Length(fieldName = "title") + @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() { @@ -37,7 +43,7 @@ public class Customer extends ContactBased implements HasTenantId { public Customer(CustomerId id) { super(id); } - + public Customer(Customer customer) { super(customer); this.tenantId = customer.getTenantId(); @@ -51,7 +57,7 @@ public class Customer extends ContactBased implements HasTenantId { public void setTenantId(TenantId tenantId) { this.tenantId = tenantId; } - + public String getTitle() { return title; } @@ -60,6 +66,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 +151,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/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index 556f13d18e..ddf7bbac36 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 @@ -21,6 +21,7 @@ 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; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -32,7 +33,9 @@ public class DashboardInfo extends SearchTextBased implements HasNa private TenantId tenantId; @NoXss + @Length(fieldName = "title") private String title; + @Length(fieldName = "image", max = 1000000) private String image; @Valid private Set assignedCustomers; 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 27951402ce..7a1a16bfe0 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 @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; @@ -44,10 +45,13 @@ public class Device extends SearchTextBasedWithAdditionalInfo implemen private TenantId tenantId; private CustomerId customerId; @NoXss + @Length(fieldName = "name") private String name; @NoXss + @Length(fieldName = "type") private String type; @NoXss + @Length(fieldName = "label") private String label; private DeviceProfileId deviceProfileId; private transient DeviceData deviceData; @@ -92,10 +96,10 @@ 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." ) + @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(); 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..cf916d9c74 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,8 +17,11 @@ 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.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.DashboardId; @@ -26,6 +29,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import javax.validation.Valid; @@ -34,34 +38,56 @@ import java.io.IOException; import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; +@ApiModel @Data +@ToString(exclude = {"image", "profileDataBytes"}) @EqualsAndHashCode(callSuper = true) @Slf4j public class DeviceProfile extends SearchTextBased implements HasName, HasTenantId, HasOtaPackage { + private static final long serialVersionUID = 6998485460273302018L; + + @ApiModelProperty(position = 3, value = "JSON object with Tenant Id that owns the profile.", readOnly = true) private TenantId tenantId; @NoXss + @Length(fieldName = "name") + @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; + @Length(fieldName = "image", max = 1000000) + @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 +114,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..e9f4cc0100 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; @@ -28,12 +29,16 @@ import java.util.UUID; @Value @EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) +@ToString(callSuper = true, exclude = "image") 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..644633e1e6 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; @@ -23,6 +25,7 @@ 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.objects.TelemetryEntityView; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; /** @@ -37,15 +40,23 @@ 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 + @Length(fieldName = "name") + @ApiModelProperty(position = 5, required = true, value = "Entity View name", example = "A4B72CCDFF33") private String name; @NoXss + @Length(fieldName = "type") + @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 +84,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 +95,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/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..7b63c0dba0 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,23 @@ 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; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; +import org.thingsboard.server.common.data.ota.OtaPackageType; +@ApiModel @Slf4j @Data @EqualsAndHashCode(callSuper = true) @@ -32,21 +40,46 @@ 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 + @Length(fieldName = "title") + @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; + @NoXss + @Length(fieldName = "resourceKey") + @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 +66,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..3db0dea9d7 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,31 @@ 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.Length; 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; + @Length(fieldName = "title") @NoXss + @ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A") private String title; @NoXss + @Length(fieldName = "region") + @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() { @@ -40,7 +51,7 @@ public class Tenant extends ContactBased implements HasTenantId { public Tenant(TenantId id) { super(id); } - + public Tenant(Tenant tenant) { super(tenant); this.title = tenant.getTitle(); @@ -63,6 +74,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 +101,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..e38555f964 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,12 +17,15 @@ 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; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.io.ByteArrayInputStream; @@ -31,18 +34,30 @@ 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 { + private static final long serialVersionUID = 3021989561267192281L; + @NoXss + @Length(fieldName = "name") + @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 +80,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/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java index f6c4c88eec..982e500e0e 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 @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; - +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @@ -40,8 +40,10 @@ public class User extends SearchTextBasedWithAdditionalInfo implements H private String email; private Authority authority; @NoXss + @Length(fieldName = "first name") private String firstName; @NoXss + @Length(fieldName = "last name") private String lastName; public User() { 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..827da0cb50 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; @@ -28,29 +30,49 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; 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") + @Length(fieldName = "type") 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 +103,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/alarm/EntityAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java new file mode 100644 index 0000000000..f6d4a0e2c7 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java @@ -0,0 +1,40 @@ +/** + * 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.alarm; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EntityAlarm implements HasTenantId { + + private TenantId tenantId; + private EntityId entityId; + private long createdTime; + private String alarmType; + + private CustomerId customerId; + private AlarmId alarmId; + +} 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 6caee2f111..2cc2eccd9d 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 @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; import java.util.Optional; @@ -39,10 +40,13 @@ public class Asset extends SearchTextBasedWithAdditionalInfo implements private TenantId tenantId; private CustomerId customerId; @NoXss + @Length(fieldName = "name") private String name; @NoXss + @Length(fieldName = "type") private String type; @NoXss + @Length(fieldName = "label") private String label; public Asset() { 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/data/DeviceData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java index 06e52d47a0..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 @@ -16,13 +16,16 @@ 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/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..ac77d5afc0 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,42 @@ */ 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.Length; 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; + @Length(fieldName = "alarm type") @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/ProtoTransportPayloadConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java index d3836a35a0..08a0cdd610 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/ProtoTransportPayloadConfiguration.java @@ -54,6 +54,9 @@ public class ProtoTransportPayloadConfiguration implements TransportPayloadTypeC private String deviceRpcRequestProtoSchema; private String deviceRpcResponseProtoSchema; + private boolean enableCompatibilityWithJsonPayloadFormat; + private boolean useJsonPayloadFormatForDefaultDownlinkTopics; + @Override public TransportPayloadType getTransportPayloadType() { return TransportPayloadType.PROTOBUF; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index 7a858f8325..b7bab9baf7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -15,8 +15,9 @@ */ package org.thingsboard.server.common.data.edge; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.HasCustomerId; @@ -27,10 +28,12 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; +@ApiModel @EqualsAndHashCode(callSuper = true) @ToString -@Getter @Setter public class Edge extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { @@ -39,12 +42,26 @@ public class Edge extends SearchTextBasedWithAdditionalInfo implements H private TenantId tenantId; private CustomerId customerId; private RuleChainId rootRuleChainId; + @NoXss + @Length(fieldName = "name") private String name; + @NoXss + @Length(fieldName = "type") private String type; + @NoXss + @Length(fieldName = "label") private String label; + @NoXss + @Length(fieldName = "routingKey") private String routingKey; + @NoXss + @Length(fieldName = "secret") private String secret; + @NoXss + @Length(fieldName = "edgeLicenseKey", max = 30) private String edgeLicenseKey; + @NoXss + @Length(fieldName = "cloudEndpoint") private String cloudEndpoint; public Edge() { @@ -82,8 +99,77 @@ public class Edge extends SearchTextBasedWithAdditionalInfo implements H this.cloudEndpoint = edge.getCloudEndpoint(); } + @ApiModelProperty(position = 1, value = "JSON object with the Edge Id. " + + "Specify this field to update the Edge. " + + "Referencing non-existing Edge Id will cause error. " + + "Omit this field to create new Edge." ) + @Override + public EdgeId getId() { + return super.getId(); + } + + @ApiModelProperty(position = 2, value = "Timestamp of the edge 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) + @Override + public TenantId getTenantId() { + return this.tenantId; + } + + @ApiModelProperty(position = 4, value = "JSON object with Customer Id. Use 'assignEdgeToCustomer' to change the Customer Id.", readOnly = true) + @Override + public CustomerId getCustomerId() { + return this.customerId; + } + + @ApiModelProperty(position = 5, value = "JSON object with Root Rule Chain Id. Use 'setEdgeRootRuleChain' to change the Root Rule Chain Id.", readOnly = true) + public RuleChainId getRootRuleChainId() { + return this.rootRuleChainId; + } + + @ApiModelProperty(position = 6, required = true, value = "Unique Edge Name in scope of Tenant", example = "Silo_A_Edge") + @Override + public String getName() { + return this.name; + } + + @ApiModelProperty(position = 7, required = true, value = "Edge type", example = "Silos") + public String getType() { + return this.type; + } + + @ApiModelProperty(position = 8, value = "Label that may be used in widgets", example = "Silo Edge on far field") + public String getLabel() { + return this.label; + } + @Override public String getSearchText() { return getName(); } + + @ApiModelProperty(position = 9, required = true, value = "Edge routing key ('username') to authorize on cloud") + public String getRoutingKey() { + return this.routingKey; + } + + @ApiModelProperty(position = 10, required = true, value = "Edge secret ('password') to authorize on cloud") + public String getSecret() { + return this.secret; + } + + @ApiModelProperty(position = 11, required = true, value = "Edge license key obtained from license portal", example = "AgcnI24Z06XC&m6Sxsdgf") + public String getEdgeLicenseKey() { + return this.edgeLicenseKey; + } + + @ApiModelProperty(position = 12, required = true, value = "Edge uses this cloud URL to activate and periodically check it's license", example = "https://thingsboard.cloud") + public String getCloudEndpoint() { + return this.cloudEndpoint; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java index 0da7fd08af..e5c941709c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeSearchQuery.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.edge; +import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -28,8 +29,11 @@ import java.util.List; @Data public class EdgeSearchQuery { + @ApiModelProperty(position = 3, value = "Main search parameters.") private RelationsSearchParameters parameters; + @ApiModelProperty(position = 1, value = "Type of the relation between root entity and edge (e.g. 'Contains' or 'Manages').") private String relationType; + @ApiModelProperty(position = 2, value = "Array of edge types to filter the related entities (e.g. 'Silos', 'Stores').") private List edgeTypes; public EntityRelationsQuery toEntitySearchQuery() { 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/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java index 6057858fcd..d27aa43c7e 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 = "ID of the entity, time-based UUID v1", example = "784f394c-42b6-435a-983c-b7beff2784f9") UUID getId(); + @ApiModelProperty(position = 2, required = true, example = "DEVICE") EntityType getEntityType(); @JsonIgnore diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java index d87c260898..eaca069e95 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java @@ -15,8 +15,7 @@ */ package org.thingsboard.server.common.data.kv; -import com.fasterxml.jackson.databind.JsonNode; - +import javax.validation.Valid; import java.util.Optional; /** @@ -24,7 +23,10 @@ import java.util.Optional; */ public class BaseAttributeKvEntry implements AttributeKvEntry { + private static final long serialVersionUID = -6460767583563159407L; + private final long lastUpdateTs; + @Valid private final KvEntry kv; public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java index a04c29f82b..9c2edb6250 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java @@ -15,11 +15,14 @@ */ package org.thingsboard.server.common.data.kv; +import org.thingsboard.server.common.data.validation.Length; + import java.util.Objects; import java.util.Optional; public abstract class BasicKvEntry implements KvEntry { + @Length(fieldName = "attribute key") private final String key; protected BasicKvEntry(String key) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java index 407be7ccfa..d7e27e83d1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java @@ -21,6 +21,7 @@ import java.util.Optional; public class StringDataEntry extends BasicKvEntry { private static final long serialVersionUID = 1L; + private final String value; public StringDataEntry(String key, String value) { 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..937d88b1c1 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,43 @@ */ 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; +import org.thingsboard.server.common.data.validation.Length; @Builder(toBuilder = true) @EqualsAndHashCode @Data @ToString +@ApiModel public class OAuth2BasicMapperConfig { + @Length(fieldName = "emailAttributeKey", max = 31) + @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; + @Length(fieldName = "firstNameAttributeKey", max = 31) + @ApiModelProperty(value = "First name attribute key") private final String firstNameAttributeKey; + @Length(fieldName = "lastNameAttributeKey", max = 31) + @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; + @Length(fieldName = "tenantNamePattern") + @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; + @Length(fieldName = "customerNamePattern") + @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; + @Length(fieldName = "defaultDashboardName") + @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..b4070313e8 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; @@ -22,27 +24,54 @@ import lombok.ToString; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; +import org.thingsboard.server.common.data.validation.Length; +import javax.validation.Valid; import java.util.List; @EqualsAndHashCode(callSuper = true) @Data @ToString @NoArgsConstructor +@ApiModel public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName { + @Length(fieldName = "providerId") + @ApiModelProperty(value = "OAuth2 provider identifier (e.g. its name)", required = true) private String providerId; + @Valid + @ApiModelProperty(value = "Default config for mapping OAuth2 log in response to platform entities") private OAuth2MapperConfig mapperConfig; + @Length(fieldName = "authorizationUri") + @ApiModelProperty(value = "Default authorization URI of the OAuth2 provider") private String authorizationUri; + @Length(fieldName = "accessTokenUri") + @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; + @Length(fieldName = "userInfoUri") + @ApiModelProperty(value = "Default user info URI of the OAuth2 provider") private String userInfoUri; + @Length(fieldName = "userNameAttributeName") + @ApiModelProperty(value = "Default name of the username attribute in OAuth2 provider log in response") private String userNameAttributeName; + @Length(fieldName = "jwkSetUri") + @ApiModelProperty(value = "Default JSON Web Key URI of the OAuth2 provider") private String jwkSetUri; + @Length(fieldName = "clientAuthenticationMethod") + @ApiModelProperty(value = "Default client authentication method to use: 'BASIC' or 'POST'") private String clientAuthenticationMethod; + @ApiModelProperty(value = "Comment for OAuth2 provider") private String comment; + @Length(fieldName = "loginButtonIcon") + @ApiModelProperty(value = "Default log in button icon for OAuth2 provider") private String loginButtonIcon; + @Length(fieldName = "loginButtonLabel") + @ApiModelProperty(value = "Default OAuth2 provider label") private String loginButtonLabel; + @Length(fieldName = "helpLink") + @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/OAuth2CustomMapperConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java index 2dac9b4100..e1bab24619 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2CustomMapperConfig.java @@ -15,15 +15,22 @@ */ package org.thingsboard.server.common.data.oauth2; -import lombok.*; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.validation.Length; @Builder(toBuilder = true) @EqualsAndHashCode @Data @ToString(exclude = {"password"}) public class OAuth2CustomMapperConfig { + @Length(fieldName = "url") private final String url; + @Length(fieldName = "username") private final String username; + @Length(fieldName = "password") private final String password; private final boolean sendToken; } 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..9790334efb 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,19 +15,29 @@ */ package org.thingsboard.server.common.data.oauth2; +import io.swagger.annotations.ApiModelProperty; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; +import javax.validation.Valid; + @Builder(toBuilder = true) @EqualsAndHashCode @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; + @Valid + @ApiModelProperty(value = "Mapper config for BASIC and GITHUB mapper types") private OAuth2BasicMapperConfig basic; + @Valid + @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/TimePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java index 879db38218..be4878b50e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java @@ -17,8 +17,12 @@ package org.thingsboard.server.common.data.page; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; @Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class TimePageLink extends PageLink { private final Long startTime; 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 5c856cf38d..9ee3320069 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 @@ -21,6 +21,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.*; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.ComponentDescriptorId; +import org.thingsboard.server.common.data.validation.Length; /** * @author Andrew Shvayka @@ -35,12 +36,14 @@ public class ComponentDescriptor extends SearchTextBased @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; + @Length(fieldName = "name") @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; + @Length(fieldName = "actions") @ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", readOnly = true) @Getter @Setter private String actions; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java index 26c61a03bf..cdb45538ea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java @@ -20,6 +20,6 @@ package org.thingsboard.server.common.data.plugin; */ public enum ComponentType { - ENRICHMENT, FILTER, TRANSFORMATION, ACTION, EXTERNAL + ENRICHMENT, FILTER, TRANSFORMATION, ACTION, EXTERNAL, FLOW } 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..040f99f1f1 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,18 @@ 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 org.thingsboard.server.common.data.validation.Length; -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; @@ -38,6 +38,7 @@ public class EntityRelation implements Serializable { private EntityId from; private EntityId to; + @Length(fieldName = "type") private String type; private RelationTypeGroup typeGroup; private transient JsonNode additionalInfo; @@ -72,6 +73,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 +82,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 +91,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 +100,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 +109,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/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java index c6ca10088d..c35a1dfd1a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java @@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.relation; public enum RelationTypeGroup { COMMON, - ALARM, DASHBOARD, RULE_CHAIN, RULE_NODE, 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 f195840f3b..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,7 @@ */ 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; @@ -33,7 +34,7 @@ import java.util.UUID; @AllArgsConstructor public class RelationsSearchParameters { - @ApiModelProperty(position = 1, value = "Root entity id to start search from.") + @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; @@ -59,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..bd387db7ee 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; @@ -26,8 +28,10 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) @Slf4j @@ -35,13 +39,21 @@ 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 + @Length(fieldName = "name") + @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 +87,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 +109,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..2193e86f66 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,26 +16,37 @@ 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; +import javax.validation.Valid; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * 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; + @Valid + @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/RuleChainOutputLabelsUsage.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java new file mode 100644 index 0000000000..3e0bde1c94 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainOutputLabelsUsage.java @@ -0,0 +1,45 @@ +/** + * 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.rule; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; + +import java.util.Set; + +@ApiModel +@Data +@Slf4j +public class RuleChainOutputLabelsUsage { + + @ApiModelProperty(position = 1, required = true, value = "Rule Chain Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private RuleChainId ruleChainId; + @ApiModelProperty(position = 2, required = true, value = "Rule Node Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private RuleNodeId ruleNodeId; + + @ApiModelProperty(position = 3, required = true, value = "Rule Chain Name", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private String ruleChainName; + @ApiModelProperty(position = 4, required = true, value = "Rule Node Name", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private String ruleNodeName; + @ApiModelProperty(position = 5, required = true, value = "Output labels", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private Set labels; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainUpdateResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainUpdateResult.java new file mode 100644 index 0000000000..76e6ce9870 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainUpdateResult.java @@ -0,0 +1,44 @@ +/** + * 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.rule; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.id.RuleNodeId; + +import java.util.List; +import java.util.Map; + +/** + * Created by igor on 3/13/18. + */ +@Data +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class RuleChainUpdateResult { + + private final boolean success; + private final List updatedRuleNodes; + + public static RuleChainUpdateResult failed(){ + return new RuleChainUpdateResult(false, null); + } + + public static RuleChainUpdateResult successful(List updatedRuleNodes){ + return new RuleChainUpdateResult(true, updatedRuleNodes); + } + +} 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..8201807808 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; @@ -24,7 +26,9 @@ import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.validation.Length; +@ApiModel @Data @EqualsAndHashCode(callSuper = true) @Slf4j @@ -32,10 +36,17 @@ 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; + @Length(fieldName = "type") + @ApiModelProperty(position = 4, value = "Full Java Class Name of the rule node implementation. ", example = "com.mycompany.iot.rule.engine.ProcessingNode") private String type; + @Length(fieldName = "name") + @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 +86,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/rule/RuleNodeUpdateResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeUpdateResult.java new file mode 100644 index 0000000000..02f68ea5c2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNodeUpdateResult.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.common.data.rule; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import org.thingsboard.server.common.data.id.RuleNodeId; + +import java.util.Map; + +/** + * Created by igor on 3/13/18. + */ +@Data +public class RuleNodeUpdateResult { + + private final RuleNode oldRuleNode; + private final RuleNode newRuleNode; + +} 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 5c1972d1e1..7298154162 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 @@ -25,6 +25,8 @@ import java.io.Serializable; @Data public class SecuritySettings implements Serializable { + private static final long serialVersionUID = -1307613974597312465L; + @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." ) 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/validation/Length.java b/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java new file mode 100644 index 0000000000..58121de31d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/validation/Length.java @@ -0,0 +1,38 @@ +/** + * 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.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Constraint(validatedBy = {}) +public @interface Length { + String message() default "length of {fieldName} must be equal or less than {max}"; + + String fieldName(); + + int max() default 255; + + Class[] groups() default {}; + + Class[] payload() default {}; +} 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..89452ffdbc 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,11 +15,13 @@ */ 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; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; +import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @Data @@ -27,12 +29,19 @@ 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 + @Length(fieldName = "bundleAlias") + @ApiModelProperty(position = 4, value = "Reference to widget bundle", readOnly = true) private String bundleAlias; @NoXss + @Length(fieldName = "alias") + @ApiModelProperty(position = 5, value = "Unique alias that is used in dashboards as a reference widget type", readOnly = true) private String alias; @NoXss + @Length(fieldName = "name") + @ApiModelProperty(position = 6, value = "Widget name used in search and UI", readOnly = true) private String name; public BaseWidgetType() { @@ -50,4 +59,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..8c3813d5c1 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,16 +16,22 @@ 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.Length; import org.thingsboard.server.common.data.validation.NoXss; @Data @JsonPropertyOrder({ "alias", "name", "image", "description", "descriptor" }) public class WidgetTypeDetails extends WidgetType { + @Length(fieldName = "image", max = 1000000) + @ApiModelProperty(position = 8, value = "Base64 encoded thumbnail", readOnly = true) private String image; @NoXss + @Length(fieldName = "description") + @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..d0df03702e 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,25 +15,52 @@ */ 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; import org.thingsboard.server.common.data.id.WidgetsBundleId; +import org.thingsboard.server.common.data.validation.Length; 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 + @Length(fieldName = "alias") + @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 + @Length(fieldName = "title") + @Getter + @Setter + @ApiModelProperty(position = 5, value = "Title used in search and UI", readOnly = true) private String title; + + @Length(fieldName = "image", max = 1000000) + @Getter + @Setter + @ApiModelProperty(position = 6, value = "Base64 encoded thumbnail", readOnly = true) private String image; + @NoXss + @Length(fieldName = "description") + @Getter + @Setter + @ApiModelProperty(position = 7, value = "Description", readOnly = true) private String description; public WidgetsBundle() { @@ -53,42 +80,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/edge-api/pom.xml b/common/edge-api/pom.xml index 4223b92d70..482e9d77d8 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 1916aeb254..92c216d7b0 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 105c624eba..eccf46b0cc 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -61,6 +61,16 @@ public enum MsgType { */ RULE_CHAIN_TO_RULE_CHAIN_MSG, + /** + * Message that is sent by RuleNodeActor as input to other RuleChain with command to process TbMsg. + */ + RULE_CHAIN_INPUT_MSG, + + /** + * Message that is sent by RuleNodeActor as output to RuleNode in other RuleChain with command to process TbMsg. + */ + RULE_CHAIN_OUTPUT_MSG, + /** * Message that is sent by RuleActor to RuleChainActor with command to process TbMsg by next nodes in chain. */ 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 83715994da..63c148b951 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 @@ -55,24 +55,25 @@ public final class TbMsg implements Serializable { private final RuleChainId ruleChainId; private final RuleNodeId ruleNodeId; @Getter(value = AccessLevel.NONE) - private final AtomicInteger ruleNodeExecCounter; - - - public int getAndIncrementRuleNodeCounter() { - return ruleNodeExecCounter.getAndIncrement(); - } + @JsonIgnore + //This field is not serialized because we use queues and there is no need to do it + private final TbMsgProcessingCtx ctx; //This field is not serialized because we use queues and there is no need to do it @JsonIgnore transient private final TbMsgCallback callback; + public int getAndIncrementRuleNodeCounter() { + return ctx.getAndIncrementRuleNodeCounter(); + } + public static TbMsg newMsg(String queueName, String type, EntityId originator, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return newMsg(queueName, type, originator, null, metaData, data, ruleChainId, ruleNodeId); } public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, - metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, 0, TbMsgCallback.EMPTY); + metaData.copy(), TbMsgDataType.JSON, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data) { @@ -80,7 +81,8 @@ public final class TbMsg implements Serializable { } public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, 0, TbMsgCallback.EMPTY); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } // REALLY NEW MSG @@ -90,11 +92,13 @@ public final class TbMsg implements Serializable { } public static TbMsg newMsg(String queueName, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, String data) { - return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), TbMsgDataType.JSON, data, null, null, 0, TbMsgCallback.EMPTY); + return new TbMsg(queueName, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, metaData.copy(), dataType, data, null, null, 0, TbMsgCallback.EMPTY); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, customerId, + metaData.copy(), dataType, data, null, null, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data) { @@ -104,46 +108,48 @@ public final class TbMsg implements Serializable { // For Tests only public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, TbMsgDataType dataType, String data, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), dataType, data, ruleChainId, ruleNodeId, 0, TbMsgCallback.EMPTY); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + metaData.copy(), dataType, data, ruleChainId, ruleNodeId, null, TbMsgCallback.EMPTY); } public static TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data, TbMsgCallback callback) { - return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, metaData.copy(), TbMsgDataType.JSON, data, null, null, 0, callback); + return new TbMsg(ServiceQueue.MAIN, UUID.randomUUID(), System.currentTimeMillis(), type, originator, null, + metaData.copy(), TbMsgDataType.JSON, data, null, null, null, callback); } public static TbMsg transformMsg(TbMsg tbMsg, String type, EntityId originator, TbMsgMetaData metaData, String data) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, type, originator, tbMsg.customerId, metaData.copy(), tbMsg.dataType, - data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ruleNodeExecCounter.get(), tbMsg.callback); + data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback); } public static TbMsg transformMsg(TbMsg tbMsg, CustomerId customerId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); + tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId) { return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, ruleChainId, null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); + tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, String queueName) { return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); + tbMsg.data, tbMsg.getRuleChainId(), null, tbMsg.ctx.copy(), tbMsg.getCallback()); } public static TbMsg transformMsg(TbMsg tbMsg, RuleChainId ruleChainId, String queueName) { return new TbMsg(queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType, - tbMsg.data, ruleChainId, null, tbMsg.ruleNodeExecCounter.get(), tbMsg.getCallback()); + tbMsg.data, ruleChainId, null, tbMsg.ctx.copy(), tbMsg.getCallback()); } //used for enqueueForTellNext public static TbMsg newMsg(TbMsg tbMsg, String queueName, RuleChainId ruleChainId, RuleNodeId ruleNodeId) { return new TbMsg(queueName, UUID.randomUUID(), tbMsg.getTs(), tbMsg.getType(), tbMsg.getOriginator(), tbMsg.customerId, tbMsg.getMetaData().copy(), - tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ruleNodeExecCounter.get(), TbMsgCallback.EMPTY); + tbMsg.getDataType(), tbMsg.getData(), ruleChainId, ruleNodeId, tbMsg.ctx.copy(), TbMsgCallback.EMPTY); } private TbMsg(String queueName, UUID id, long ts, String type, EntityId originator, CustomerId customerId, TbMsgMetaData metaData, TbMsgDataType dataType, String data, - RuleChainId ruleChainId, RuleNodeId ruleNodeId, int ruleNodeExecCounter, TbMsgCallback callback) { + RuleChainId ruleChainId, RuleNodeId ruleNodeId, TbMsgProcessingCtx ctx, TbMsgCallback callback) { this.id = id; this.queueName = queueName != null ? queueName : ServiceQueue.MAIN; if (ts > 0) { @@ -167,7 +173,7 @@ public final class TbMsg implements Serializable { this.data = data; this.ruleChainId = ruleChainId; this.ruleNodeId = ruleNodeId; - this.ruleNodeExecCounter = new AtomicInteger(ruleNodeExecCounter); + this.ctx = ctx != null ? ctx : new TbMsgProcessingCtx(); if (callback != null) { this.callback = callback; } else { @@ -209,7 +215,8 @@ public final class TbMsg implements Serializable { builder.setDataType(msg.getDataType().ordinal()); builder.setData(msg.getData()); - builder.setRuleNodeExecCounter(msg.ruleNodeExecCounter.get()); + + builder.setCtx(msg.ctx.toProto()); return builder.build().toByteArray(); } @@ -231,8 +238,17 @@ public final class TbMsg implements Serializable { ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); } + TbMsgProcessingCtx ctx; + if (proto.hasCtx()) { + ctx = TbMsgProcessingCtx.fromProto(proto.getCtx()); + } else { + // Backward compatibility with unprocessed messages fetched from queue after update. + ctx = new TbMsgProcessingCtx(proto.getRuleNodeExecCounter()); + } + TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; - return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, proto.getRuleNodeExecCounter(), callback); + return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), proto.getType(), entityId, customerId, + metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, ctx, callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); } @@ -243,11 +259,13 @@ public final class TbMsg implements Serializable { } public TbMsg copyWithRuleChainId(RuleChainId ruleChainId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, null, this.ruleNodeExecCounter.get(), callback); + return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + this.metaData, this.dataType, this.data, ruleChainId, null, this.ctx, callback); } public TbMsg copyWithRuleNodeId(RuleChainId ruleChainId, RuleNodeId ruleNodeId, UUID msgId) { - return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ruleNodeExecCounter.get(), callback); + return new TbMsg(this.queueName, msgId, this.ts, this.type, this.originator, this.customerId, + this.metaData, this.dataType, this.data, ruleChainId, ruleNodeId, this.ctx, callback); } public TbMsgCallback getCallback() { @@ -262,4 +280,12 @@ public final class TbMsg implements Serializable { public String getQueueName() { return queueName != null ? queueName : ServiceQueue.MAIN; } + + public void pushToStack(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + ctx.push(ruleChainId, ruleNodeId); + } + + public TbMsgProcessingStackItem popFormStack() { + return ctx.pop(); + } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.java new file mode 100644 index 0000000000..47bcf683b0 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingCtx.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.common.msg; + +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.msg.gen.MsgProtos; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Created by ashvayka on 13.01.18. + */ +public final class TbMsgProcessingCtx implements Serializable { + + private final AtomicInteger ruleNodeExecCounter; + private volatile LinkedList stack; + + public TbMsgProcessingCtx() { + this(0); + } + + public TbMsgProcessingCtx(int ruleNodeExecCounter) { + this(ruleNodeExecCounter, null); + } + + protected TbMsgProcessingCtx(int ruleNodeExecCounter, LinkedList stack) { + this.ruleNodeExecCounter = new AtomicInteger(ruleNodeExecCounter); + this.stack = stack; + } + + public int getAndIncrementRuleNodeCounter() { + return ruleNodeExecCounter.getAndIncrement(); + } + + public TbMsgProcessingCtx copy() { + if (stack == null || stack.isEmpty()) { + return new TbMsgProcessingCtx(ruleNodeExecCounter.get()); + } else { + return new TbMsgProcessingCtx(ruleNodeExecCounter.get(), new LinkedList<>(stack)); + } + } + + public void push(RuleChainId ruleChainId, RuleNodeId ruleNodeId) { + if (stack == null) { + stack = new LinkedList<>(); + } + stack.add(new TbMsgProcessingStackItem(ruleChainId, ruleNodeId)); + } + + public TbMsgProcessingStackItem pop() { + return !stack.isEmpty() ? stack.removeLast() : null; + } + + public static TbMsgProcessingCtx fromProto(MsgProtos.TbMsgProcessingCtxProto ctx) { + int ruleNodeExecCounter = ctx.getRuleNodeExecCounter(); + if (ctx.getStackCount() > 0) { + LinkedList stack = new LinkedList<>(); + for (MsgProtos.TbMsgProcessingStackItemProto item : ctx.getStackList()) { + stack.add(TbMsgProcessingStackItem.fromProto(item)); + } + return new TbMsgProcessingCtx(ruleNodeExecCounter, stack); + } else { + return new TbMsgProcessingCtx(ruleNodeExecCounter); + } + } + + public MsgProtos.TbMsgProcessingCtxProto toProto() { + var ctxBuilder = MsgProtos.TbMsgProcessingCtxProto.newBuilder(); + ctxBuilder.setRuleNodeExecCounter(ruleNodeExecCounter.get()); + if (stack != null) { + for (TbMsgProcessingStackItem item : stack) { + ctxBuilder.addStack(item.toProto()); + } + } + return ctxBuilder.build(); + } +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingStackItem.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingStackItem.java new file mode 100644 index 0000000000..6490ddf547 --- /dev/null +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsgProcessingStackItem.java @@ -0,0 +1,47 @@ +/** + * 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.msg; + +import lombok.Data; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.msg.gen.MsgProtos; + +import java.util.UUID; + +@Data +public class TbMsgProcessingStackItem { + + private final RuleChainId ruleChainId; + private final RuleNodeId ruleNodeId; + + MsgProtos.TbMsgProcessingStackItemProto toProto() { + return MsgProtos.TbMsgProcessingStackItemProto.newBuilder() + .setRuleChainIdMSB(ruleChainId.getId().getMostSignificantBits()) + .setRuleChainIdLSB(ruleChainId.getId().getLeastSignificantBits()) + .setRuleNodeIdMSB(ruleNodeId.getId().getMostSignificantBits()) + .setRuleNodeIdLSB(ruleNodeId.getId().getLeastSignificantBits()) + .build(); + } + + static TbMsgProcessingStackItem fromProto(MsgProtos.TbMsgProcessingStackItemProto item){ + return new TbMsgProcessingStackItem( + new RuleChainId(new UUID(item.getRuleChainIdMSB(), item.getRuleChainIdLSB())), + new RuleNodeId(new UUID(item.getRuleNodeIdMSB(), item.getRuleNodeIdLSB())) + ); + } + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java index cd073f3a6e..44e30487d6 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/RuleNodeException.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.rule.RuleNode; public class RuleNodeException extends RuleEngineException { private static final long serialVersionUID = -1776681087370749776L; + public static final String UNKNOWN = "Unknown"; @Getter private final String ruleChainName; @@ -45,7 +46,7 @@ public class RuleNodeException extends RuleEngineException { this.ruleChainId = ruleNode.getRuleChainId(); this.ruleNodeId = ruleNode.getId(); } else { - ruleNodeName = "Unknown"; + ruleNodeName = UNKNOWN; ruleChainId = new RuleChainId(RuleChainId.NULL_UUID); ruleNodeId = new RuleNodeId(RuleNodeId.NULL_UUID); } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java index 89b2c7b527..35d5ad63c4 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java @@ -26,6 +26,7 @@ public class TbRateLimitsException extends RuntimeException { private final EntityType entityType; public TbRateLimitsException(EntityType entityType) { + super(entityType.name() + " rate limits reached!"); this.entityType = entityType; } } diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto index 75f5051fbf..4d7682634b 100644 --- a/common/message/src/main/proto/tbmsg.proto +++ b/common/message/src/main/proto/tbmsg.proto @@ -19,10 +19,24 @@ package msgqueue; option java_package = "org.thingsboard.server.common.msg.gen"; option java_outer_classname = "MsgProtos"; +// Stores message metadata as map of strings message TbMsgMetaDataProto { map data = 1; } +// Stores stack of nested (caller) rule chains +message TbMsgProcessingStackItemProto { + int64 ruleChainIdMSB = 1; + int64 ruleChainIdLSB = 2; + int64 ruleNodeIdMSB = 3; + int64 ruleNodeIdLSB = 4; +} + +message TbMsgProcessingCtxProto { + int32 ruleNodeExecCounter = 1; + repeated TbMsgProcessingStackItemProto stack = 2; +} + message TbMsgProto { string id = 1; string type = 2; @@ -39,14 +53,17 @@ message TbMsgProto { TbMsgMetaDataProto metaData = 11; - //Transaction Data (12) was removed in 2.5 + // Transaction Data (12) was removed in 2.5 int32 dataType = 13; string data = 14; int64 ts = 15; + // Will be removed in 3.4. Moved to processing context int32 ruleNodeExecCounter = 16; int64 customerIdMSB = 17; int64 customerIdLSB = 18; + + TbMsgProcessingCtxProto ctx = 19; } \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index 233046824d..b318d57a4e 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 41937ac079..a43e5c0188 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common 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/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 5dd7c40ba8..0dbb2401b3 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.TbProperty; import java.util.Collections; import java.util.List; @@ -104,10 +105,10 @@ public class TbKafkaSettings { private String securityProtocol; @Setter - private List other; + private List other; @Setter - private Map> consumerPropertiesPerTopic = Collections.emptyMap(); + private Map> consumerPropertiesPerTopic = Collections.emptyMap(); public Properties toAdminProps() { Properties props = toProps(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java index d89b03c744..026f879c8d 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueFactory.java @@ -117,7 +117,7 @@ public class AwsSqsMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic(), + return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java index 172f27d357..0e9f1812dd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbRuleEngineQueueFactory.java @@ -109,7 +109,7 @@ public class AwsSqsTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, ruleEngineSettings.getTopic(), + return new TbAwsSqsConsumerTemplate<>(ruleEngineAdmin, sqsSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java index cd1c34b612..81d2ba795a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/InMemoryMonolithQueueFactory.java @@ -92,7 +92,7 @@ public class InMemoryMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new InMemoryTbQueueConsumer<>(ruleEngineSettings.getTopic()); + return new InMemoryTbQueueConsumer<>(configuration.getTopic()); } @Override diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index 3bb0a53ad1..e08be485ea 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -160,7 +160,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi String queueName = configuration.getName(); TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(ruleEngineSettings.getTopic()); + consumerBuilder.topic(configuration.getTopic()); consumerBuilder.clientId("re-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId("re-" + queueName + "-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java index fce429c9a3..58bc38d3a9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbRuleEngineQueueFactory.java @@ -159,7 +159,7 @@ public class KafkaTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory { String queueName = configuration.getName(); TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); - consumerBuilder.topic(ruleEngineSettings.getTopic()); + consumerBuilder.topic(configuration.getTopic()); consumerBuilder.clientId("re-" + queueName + "-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId("re-" + queueName + "-consumer"); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java index 3f014f578c..1d2f36c5c2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubMonolithQueueFactory.java @@ -127,7 +127,7 @@ public class PubSubMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEng @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, ruleEngineSettings.getTopic(), + return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java index e82de20417..b02322496f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/PubSubTbRuleEngineQueueFactory.java @@ -114,7 +114,7 @@ public class PubSubTbRuleEngineQueueFactory implements TbRuleEngineQueueFactory @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, ruleEngineSettings.getTopic(), + return new TbPubSubConsumerTemplate<>(ruleEngineAdmin, pubSubSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java index 7730f8f5dd..7d48803258 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqMonolithQueueFactory.java @@ -125,7 +125,7 @@ public class RabbitMqMonolithQueueFactory implements TbCoreQueueFactory, TbRuleE @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic(), + return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java index 09c0640fb8..2f8a3ed655 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/RabbitMqTbRuleEngineQueueFactory.java @@ -112,7 +112,7 @@ public class RabbitMqTbRuleEngineQueueFactory implements TbRuleEngineQueueFactor @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, ruleEngineSettings.getTopic(), + return new TbRabbitMqConsumerTemplate<>(ruleEngineAdmin, rabbitMqSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java index 2443093cf0..3731a56aee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusMonolithQueueFactory.java @@ -124,7 +124,7 @@ public class ServiceBusMonolithQueueFactory implements TbCoreQueueFactory, TbRul @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbServiceBusConsumerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic(), + return new TbServiceBusConsumerTemplate<>(ruleEngineAdmin, serviceBusSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java index 52858882cb..be11736ad2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/ServiceBusTbRuleEngineQueueFactory.java @@ -112,7 +112,7 @@ public class ServiceBusTbRuleEngineQueueFactory implements TbRuleEngineQueueFact @Override public TbQueueConsumer> createToRuleEngineMsgConsumer(TbRuleEngineQueueConfiguration configuration) { - return new TbServiceBusConsumerTemplate<>(ruleEngineAdmin, serviceBusSettings, ruleEngineSettings.getTopic(), + return new TbServiceBusConsumerTemplate<>(ruleEngineAdmin, serviceBusSettings, configuration.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java index 370de259ef..92890da39f 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageClient.java @@ -125,7 +125,7 @@ public class DefaultTbApiUsageClient implements TbApiUsageClient { })); if (!report.isEmpty()) { - log.info("Reporting API usage statistics for {} tenants and customers", report.size()); + log.debug("Reporting API usage statistics for {} tenants and customers", report.size()); } } diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 8501fea19a..2af921b251 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index a1e1aaa2b0..26f1f45a0f 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index cf8c40a51c..78b25c2d42 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -223,9 +223,7 @@ public class CoapTransportResource extends AbstractCoapTransportResource { return; } transportService.process(DeviceTransportType.COAP, TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(credentials.get().getCredentialsId()).build(), - new CoapDeviceAuthCallback(exchange, (deviceCredentials, deviceProfile) -> { - processRequest(exchange, type, request, deviceCredentials, deviceProfile); - })); + new CoapDeviceAuthCallback(exchange, (deviceCredentials, deviceProfile) -> processRequest(exchange, type, request, deviceCredentials, deviceProfile))); } private void processRequest(CoapExchange exchange, SessionMsgType type, Request request, ValidateDeviceCredentialsResponse deviceCredentials, DeviceProfile deviceProfile) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index 66fd870dc9..6d7be52ad9 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -39,7 +39,10 @@ public class CoapTransportService implements TbTransportService { private static final String V1 = "v1"; private static final String API = "api"; private static final String EFENTO = "efento"; - private static final String MEASUREMENTS = "m"; + public static final String MEASUREMENTS = "m"; + public static final String DEVICE_INFO = "i"; + public static final String CONFIGURATION = "c"; + public static final String CURRENT_TIMESTAMP = "t"; @Autowired private CoapServerService coapServerService; @@ -56,9 +59,11 @@ public class CoapTransportService implements TbTransportService { CoapResource api = new CoapResource(API); api.add(new CoapTransportResource(coapTransportContext, coapServerService, V1)); - CoapResource efento = new CoapResource(EFENTO); - CoapEfentoTransportResource efentoMeasurementsTransportResource = new CoapEfentoTransportResource(coapTransportContext, MEASUREMENTS); - efento.add(efentoMeasurementsTransportResource); + CoapEfentoTransportResource efento = new CoapEfentoTransportResource(coapTransportContext, EFENTO); + efento.add(new CoapResource(MEASUREMENTS)); + efento.add(new CoapResource(DEVICE_INFO)); + efento.add(new CoapResource(CONFIGURATION)); + efento.add(new CoapResource(CURRENT_TIMESTAMP)); coapServer.add(api); coapServer.add(efento); coapServer.add(new OtaPackageTransportResource(coapTransportContext, OtaPackageType.FIRMWARE)); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java index b38ac3c322..0c7875aebd 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java @@ -39,13 +39,13 @@ public interface CoapTransportAdaptor { TransportProtos.ClaimDeviceMsg convertToClaimDevice(UUID sessionId, Request inbound, TransportProtos.SessionInfoProto sessionInfo) throws AdaptorException; - Response convertToPublish(boolean isConfirmable, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException; + Response convertToPublish(TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException; - Response convertToPublish(boolean isConfirmable, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; + Response convertToPublish(TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException; - Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException; + Response convertToPublish(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException; - Response convertToPublish(boolean isConfirmable, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException; + Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException; ProvisionDeviceRequestMsg convertToProvisionRequestMsg(UUID sessionId, Request inbound) throws AdaptorException; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index e24e14faab..d0babc51cf 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -93,21 +93,20 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException { - return getObserveNotification(isConfirmable, JsonConverter.toJson(msg)); + public Response convertToPublish(TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException { + return getObserveNotification(JsonConverter.toJson(msg)); } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg msg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { - return getObserveNotification(isConfirmable, JsonConverter.toJson(msg, true)); + public Response convertToPublish(TransportProtos.ToDeviceRpcRequestMsg msg, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { + return getObserveNotification(JsonConverter.toJson(msg, true)); } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException { + public Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException { Response response = new Response(CoAP.ResponseCode.CONTENT); JsonElement result = JsonConverter.toJson(msg); response.setPayload(result.toString()); - response.setConfirmable(isConfirmable); return response; } @@ -122,11 +121,10 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException { + public Response convertToPublish(TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException { if (msg.getSharedStateMsg()) { if (StringUtils.isEmpty(msg.getError())) { Response response = new Response(CoAP.ResponseCode.CONTENT); - response.setConfirmable(isConfirmable); TransportProtos.AttributeUpdateNotificationMsg notificationMsg = TransportProtos.AttributeUpdateNotificationMsg.newBuilder().addAllSharedUpdated(msg.getSharedAttributeListList()).build(); JsonObject result = JsonConverter.toJson(notificationMsg); response.setPayload(result.toString()); @@ -139,7 +137,6 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { return new Response(CoAP.ResponseCode.NOT_FOUND); } else { Response response = new Response(CoAP.ResponseCode.CONTENT); - response.setConfirmable(isConfirmable); JsonObject result = JsonConverter.toJson(msg); response.setPayload(result.toString()); return response; @@ -147,10 +144,9 @@ public class JsonCoapAdaptor implements CoapTransportAdaptor { } } - private Response getObserveNotification(boolean confirmable, JsonElement json) { + private Response getObserveNotification(JsonElement json) { Response response = new Response(CoAP.ResponseCode.CONTENT); response.setPayload(json.toString()); - response.setConfirmable(confirmable); return response; } 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 1ad29524d2..e4a125008e 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 @@ -113,29 +113,27 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException { - return getObserveNotification(isConfirmable, msg.toByteArray()); + public Response convertToPublish(TransportProtos.AttributeUpdateNotificationMsg msg) throws AdaptorException { + return getObserveNotification(msg.toByteArray()); } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { - return getObserveNotification(isConfirmable, ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder)); + public Response convertToPublish(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, DynamicMessage.Builder rpcRequestDynamicMessageBuilder) throws AdaptorException { + return getObserveNotification(ProtoConverter.convertToRpcRequest(rpcRequest, rpcRequestDynamicMessageBuilder)); } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException { + public Response convertToPublish(TransportProtos.ToServerRpcResponseMsg msg) throws AdaptorException { Response response = new Response(CoAP.ResponseCode.CONTENT); - response.setConfirmable(isConfirmable); response.setPayload(msg.toByteArray()); return response; } @Override - public Response convertToPublish(boolean isConfirmable, TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException { + public Response convertToPublish(TransportProtos.GetAttributeResponseMsg msg) throws AdaptorException { if (msg.getSharedStateMsg()) { if (StringUtils.isEmpty(msg.getError())) { Response response = new Response(CoAP.ResponseCode.CONTENT); - response.setConfirmable(isConfirmable); TransportProtos.AttributeUpdateNotificationMsg notificationMsg = TransportProtos.AttributeUpdateNotificationMsg.newBuilder().addAllSharedUpdated(msg.getSharedAttributeListList()).build(); response.setPayload(notificationMsg.toByteArray()); return response; @@ -147,17 +145,15 @@ public class ProtoCoapAdaptor implements CoapTransportAdaptor { return new Response(CoAP.ResponseCode.NOT_FOUND); } else { Response response = new Response(CoAP.ResponseCode.CONTENT); - response.setConfirmable(isConfirmable); response.setPayload(msg.toByteArray()); return response; } } } - private Response getObserveNotification(boolean confirmable, byte[] notification) { + private Response getObserveNotification(byte[] notification) { Response response = new Response(CoAP.ResponseCode.CONTENT); response.setPayload(notification); - response.setConfirmable(confirmable); return response; } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapEfentoCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapEfentoCallback.java new file mode 100644 index 0000000000..efda6f3efe --- /dev/null +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapEfentoCallback.java @@ -0,0 +1,51 @@ +/** + * 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.transport.coap.callback; + +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.Response; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.thingsboard.server.common.transport.TransportServiceCallback; + +public class CoapEfentoCallback implements TransportServiceCallback { + + protected final CoapExchange exchange; + protected final CoAP.ResponseCode onSuccessResponse; + protected final CoAP.ResponseCode onFailureResponse; + + public CoapEfentoCallback(CoapExchange exchange, CoAP.ResponseCode onSuccessResponse, CoAP.ResponseCode onFailureResponse) { + this.exchange = exchange; + this.onSuccessResponse = onSuccessResponse; + this.onFailureResponse = onFailureResponse; + } + + @Override + public void onSuccess(Void msg) { + //We respond only to confirmed requests in order to reduce battery consumption for Efento devices. + if (isConRequest()) { + exchange.respond(new Response(onSuccessResponse)); + } + } + + @Override + public void onError(Throwable e) { + exchange.respond(onFailureResponse); + } + + protected boolean isConRequest() { + return exchange.advanced().getRequest().isConfirmable(); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java index 40e1b894ee..491eea221e 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java @@ -34,9 +34,7 @@ public class CoapOkCallback implements TransportServiceCallback { @Override public void onSuccess(Void msg) { - Response response = new Response(onSuccessResponse); - response.setConfirmable(isConRequest()); - exchange.respond(response); + exchange.respond(new Response(onSuccessResponse)); } @Override diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java index f6e5b52ff7..f7adceec7f 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/GetAttributesSyncSessionCallback.java @@ -34,7 +34,7 @@ public class GetAttributesSyncSessionCallback extends AbstractSyncSessionCallbac @Override public void onGetAttributesResponse(TransportProtos.GetAttributeResponseMsg msg) { try { - respond(state.getAdaptor().convertToPublish(request.isConfirmable(), msg)); + respond(state.getAdaptor().convertToPublish(msg)); } catch (AdaptorException e) { log.trace("[{}] Failed to reply due to error", state.getDeviceId(), e); exchange.respond(new Response(CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java index fb4b855f39..ebf5547d24 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/ToServerRpcSyncSessionCallback.java @@ -33,7 +33,7 @@ public class ToServerRpcSyncSessionCallback extends AbstractSyncSessionCallback @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg toServerResponse) { try { - respond(state.getAdaptor().convertToPublish(request.isConfirmable(), toServerResponse)); + respond(state.getAdaptor().convertToPublish(toServerResponse)); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); exchange.respond(CoAP.ResponseCode.INTERNAL_SERVER_ERROR); diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 082a94dbe6..2a1911c232 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -435,8 +435,7 @@ public class DefaultCoapClientContext implements CoapClientContext { TbCoapObservationState attrs = state.getAttrs(); if (attrs != null) { try { - boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getAttrs()); - Response response = state.getAdaptor().convertToPublish(conRequest, msg); + Response response = state.getAdaptor().convertToPublish(msg); respond(attrs.getExchange(), response, state.getContentFormat()); } catch (AdaptorException e) { log.trace("Failed to reply due to error", e); @@ -466,7 +465,8 @@ public class DefaultCoapClientContext implements CoapClientContext { try { boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getAttrs()); int requestId = getNextMsgId(); - Response response = state.getAdaptor().convertToPublish(conRequest, msg); + Response response = state.getAdaptor().convertToPublish(msg); + response.setConfirmable(conRequest); response.setMID(requestId); if (conRequest) { response.addMessageObserver(new TbCoapMessageObserver(requestId, id -> awake(state), id -> asleep(state))); @@ -527,7 +527,8 @@ public class DefaultCoapClientContext implements CoapClientContext { String error = null; boolean conRequest = AbstractSyncSessionCallback.isConRequest(state.getRpc()); try { - Response response = state.getAdaptor().convertToPublish(conRequest, msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder()); + Response response = state.getAdaptor().convertToPublish(msg, state.getConfiguration().getRpcRequestDynamicMessageBuilder()); + response.setConfirmable(conRequest); int requestId = getNextMsgId(); response.setMID(requestId); if (conRequest) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 9fad463ea2..6fea53f48a 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -21,6 +21,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.Request; +import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Exchange; import org.eclipse.californium.core.server.resources.CoapExchange; import org.eclipse.californium.core.server.resources.Resource; @@ -38,9 +39,11 @@ import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; import org.thingsboard.server.transport.coap.CoapTransportContext; import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; +import org.thingsboard.server.transport.coap.callback.CoapEfentoCallback; import org.thingsboard.server.transport.coap.callback.CoapOkCallback; import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -48,11 +51,15 @@ import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; +import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP; +import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; +import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS; + @Slf4j public class CoapEfentoTransportResource extends AbstractCoapTransportResource { - private static final int MEASUREMENTS_POSITION = 2; - private static final String MEASUREMENTS = "m"; + private static final int CHILD_RESOURCE_POSITION = 2; public CoapEfentoTransportResource(CoapTransportContext context, String name) { super(context, name); @@ -63,7 +70,17 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { @Override protected void processHandleGet(CoapExchange exchange) { - exchange.respond(CoAP.ResponseCode.METHOD_NOT_ALLOWED); + Exchange advanced = exchange.advanced(); + Request request = advanced.getRequest(); + List uriPath = request.getOptions().getUriPath(); + boolean validPath = uriPath.size() == CHILD_RESOURCE_POSITION && uriPath.get(1).equals(CURRENT_TIMESTAMP); + if (!validPath) { + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + } else { + int dateInSec = (int) (System.currentTimeMillis() / 1000); + byte[] bytes = ByteBuffer.allocate(4).putInt(dateInSec).array(); + exchange.respond(CoAP.ResponseCode.CONTENT, bytes); + } } @Override @@ -71,11 +88,29 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { Exchange advanced = exchange.advanced(); Request request = advanced.getRequest(); List uriPath = request.getOptions().getUriPath(); - boolean validPath = uriPath.size() == MEASUREMENTS_POSITION && uriPath.get(1).equals(MEASUREMENTS); - if (!validPath) { + if (uriPath.size() != CHILD_RESOURCE_POSITION) { exchange.respond(CoAP.ResponseCode.BAD_REQUEST); return; } + String requestType = uriPath.get(1); + switch (requestType) { + case MEASUREMENTS: + processMeasurementsRequest(exchange, request); + break; + case DEVICE_INFO: + case CONFIGURATION: + //We respond only to confirmed requests in order to reduce battery consumption for Efento devices. + if (exchange.advanced().getRequest().isConfirmable()) { + exchange.respond(new Response(CoAP.ResponseCode.CREATED)); + } + break; + default: + exchange.respond(CoAP.ResponseCode.BAD_REQUEST); + break; + } + } + + private void processMeasurementsRequest(CoapExchange exchange, Request request) { byte[] bytes = request.getPayload(); try { MeasurementsProtos.ProtoMeasurements protoMeasurements = MeasurementsProtos.ProtoMeasurements.parseFrom(bytes); @@ -90,7 +125,7 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { List efentoMeasurements = getEfentoMeasurements(protoMeasurements, sessionId); transportService.process(sessionInfo, transportContext.getEfentoCoapAdaptor().convertToPostTelemetry(sessionId, efentoMeasurements), - new CoapOkCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); + new CoapEfentoCallback(exchange, CoAP.ResponseCode.CREATED, CoAP.ResponseCode.INTERNAL_SERVER_ERROR)); reportSubscriptionInfo(sessionInfo, false, false); } catch (AdaptorException e) { log.error("[{}] Failed to decode Efento ProtoMeasurements: ", sessionId, e); @@ -168,6 +203,12 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { values.addProperty("temperature_" + channel, ((double) (startPoint + sampleOffset)) / 10f); startTimestampMillis = startTimestampMillis + measurementPeriodMillis; break; + case WATER_METER: + values = valuesMap.computeIfAbsent(startTimestampMillis, k -> + CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + values.addProperty("pulse_counter_water_" + channel, ((double) (startPoint + sampleOffset))); + startTimestampMillis = startTimestampMillis + measurementPeriodMillis; + break; case HUMIDITY: values = valuesMap.computeIfAbsent(startTimestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 24e3ce2337..2164a5c58e 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.common.transport 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/pom.xml b/common/transport/lwm2m/pom.xml index 6ae5747501..33e7138e33 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java index 16f9443547..0ea9be4388 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java @@ -27,6 +27,7 @@ import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter; @@ -114,49 +115,22 @@ public class LwM2MTransportBootstrapService { } private void setServerWithCredentials(LeshanBootstrapServerBuilder builder) { - try { - if (serverConfig.getKeyStoreValue() != null) { - KeyStore keyStoreServer = serverConfig.getKeyStoreValue(); - if (this.setBuilderX509(builder)) { - X509Certificate rootCAX509Cert = (X509Certificate) keyStoreServer.getCertificate(serverConfig.getRootCertificateAlias()); - if (rootCAX509Cert != null) { - X509Certificate[] trustedCertificates = new X509Certificate[1]; - trustedCertificates[0] = rootCAX509Cert; - builder.setTrustedCertificates(trustedCertificates); - } else { - /* by default trust all */ - builder.setTrustedCertificates(new X509Certificate[0]); - } - } + if (this.bootstrapConfig.getSslCredentials() != null) { + SslCredentials sslCredentials = this.bootstrapConfig.getSslCredentials(); + builder.setPublicKey(sslCredentials.getPublicKey()); + builder.setPrivateKey(sslCredentials.getPrivateKey()); + builder.setCertificateChain(sslCredentials.getCertificateChain()); + if (this.serverConfig.getTrustSslCredentials() != null) { + builder.setTrustedCertificates(this.serverConfig.getTrustSslCredentials().getTrustedCertificates()); } else { /* by default trust all */ builder.setTrustedCertificates(new X509Certificate[0]); - log.info("Unable to load X509 files for BootStrapServer"); - this.pskMode = true; } - } catch (KeyStoreException ex) { - log.error("[{}] Unable to load X509 files server", ex.getMessage()); + } else { + /* by default trust all */ + builder.setTrustedCertificates(new X509Certificate[0]); + log.info("Unable to load X509 files for BootStrapServer"); + this.pskMode = true; } } - - private boolean setBuilderX509(LeshanBootstrapServerBuilder builder) { - try { - X509Certificate[] certificateChain = SslContextUtil.asX509Certificates(serverConfig.getKeyStoreValue().getCertificateChain(this.bootstrapConfig.getCertificateAlias())); - X509Certificate serverCertificate = certificateChain[0]; - PrivateKey privateKey = (PrivateKey) serverConfig.getKeyStoreValue().getKey(this.bootstrapConfig.getCertificateAlias(), serverConfig.getCertificatePassword() == null ? null : serverConfig.getCertificatePassword().toCharArray()); - PublicKey publicKey = serverCertificate.getPublicKey(); - if (privateKey != null && privateKey.getEncoded().length > 0 && publicKey != null && publicKey.getEncoded().length > 0) { - builder.setPublicKey(serverCertificate.getPublicKey()); - builder.setPrivateKey(privateKey); - builder.setCertificateChain(certificateChain); - return true; - } else { - return false; - } - } catch (Exception ex) { - log.error("[{}] Unable to load KeyStore files server", ex.getMessage()); - return false; - } - } - } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MSecureServerConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MSecureServerConfig.java index a525c5f157..b8a4f2c976 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MSecureServerConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MSecureServerConfig.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.transport.lwm2m.config; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; + public interface LwM2MSecureServerConfig { Integer getId(); @@ -27,8 +29,6 @@ public interface LwM2MSecureServerConfig { Integer getSecurePort(); - String getCertificateAlias(); - - String getCertificatePassword(); + SslCredentials getSslCredentials(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java index 0806ef9e90..749f7352ba 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java @@ -17,9 +17,15 @@ package org.thingsboard.server.transport.lwm2m.config; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; @Slf4j @Component @@ -46,12 +52,18 @@ public class LwM2MTransportBootstrapConfig implements LwM2MSecureServerConfig { @Value("${transport.lwm2m.bootstrap.security.bind_port:}") private Integer securePort; - @Getter - @Value("${transport.lwm2m.bootstrap.security.key_alias:}") - private String certificateAlias; + @Bean + @ConfigurationProperties(prefix = "transport.lwm2m.bootstrap.security.credentials") + public SslCredentialsConfig lwm2mBootstrapCredentials() { + return new SslCredentialsConfig("LWM2M Bootstrap DTLS Credentials", false); + } - @Getter - @Value("${transport.lwm2m.bootstrap.security.key_password:}") - private String certificatePassword; + @Autowired + @Qualifier("lwm2mBootstrapCredentials") + private SslCredentialsConfig credentialsConfig; + @Override + public SslCredentials getSslCredentials() { + return this.credentialsConfig.getCredentials(); + } } 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 bde8741f3b..11a562720a 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 @@ -18,18 +18,23 @@ package org.thingsboard.server.transport.lwm2m.config; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ResourceUtils; +import org.thingsboard.server.common.data.TbProperty; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; -import javax.annotation.PostConstruct; -import java.io.InputStream; -import java.security.KeyStore; +import java.util.List; @Slf4j @Component @ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true') || '${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core'") +@ConfigurationProperties(prefix = "transport.lwm2m") public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Getter @@ -64,26 +69,6 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Value("${transport.lwm2m.clean_period_in_sec:}") private int cleanPeriodInSec; - @Getter - @Value("${transport.lwm2m.security.key_store_type:}") - private String keyStoreType; - - @Getter - @Value("${transport.lwm2m.security.key_store:}") - private String keyStoreFilePath; - - @Getter - @Setter - private KeyStore keyStoreValue; - - @Getter - @Value("${transport.lwm2m.security.key_store_password:}") - private String keyStorePassword; - - @Getter - @Value("${transport.lwm2m.security.root_alias:}") - private String rootCertificateAlias; - @Getter @Value("${transport.lwm2m.server.id:}") private Integer id; @@ -104,18 +89,6 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Value("${transport.lwm2m.server.security.bind_port:}") private Integer securePort; - @Getter - @Value("${transport.lwm2m.server.security.key_alias:}") - private String certificateAlias; - - @Getter - @Value("${transport.lwm2m.server.security.key_password:}") - private String certificatePassword; - - @Getter - @Value("${transport.lwm2m.log_max_length:}") - private int logMaxLength; - @Getter @Value("${transport.lwm2m.psm_activity_timer:10000}") private long psmActivityTimer; @@ -124,15 +97,36 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig { @Value("${transport.lwm2m.paging_transmission_window:10000}") private long pagingTransmissionWindow; - @PostConstruct - public void init() { - try { - InputStream keyStoreInputStream = ResourceUtils.getInputStream(this, keyStoreFilePath); - keyStoreValue = KeyStore.getInstance(keyStoreType); - keyStoreValue.load(keyStoreInputStream, keyStorePassword == null ? null : keyStorePassword.toCharArray()); - } catch (Exception e) { - log.info("Unable to lookup LwM2M keystore. Reason: {}, {}", keyStoreFilePath, e.getMessage()); - } + @Getter + @Setter + private List networkConfig; + + @Bean + @ConfigurationProperties(prefix = "transport.lwm2m.server.security.credentials") + public SslCredentialsConfig lwm2mServerCredentials() { + return new SslCredentialsConfig("LWM2M Server DTLS Credentials", false); } + @Autowired + @Qualifier("lwm2mServerCredentials") + private SslCredentialsConfig credentialsConfig; + + @Bean + @ConfigurationProperties(prefix = "transport.lwm2m.security.trust-credentials") + public SslCredentialsConfig lwm2mTrustCredentials() { + return new SslCredentialsConfig("LWM2M Trust Credentials", true); + } + + @Autowired + @Qualifier("lwm2mTrustCredentials") + private SslCredentialsConfig trustCredentialsConfig; + + @Override + public SslCredentials getSslCredentials() { + return this.credentialsConfig.getCredentials(); + } + + public SslCredentials getTrustSslCredentials() { + return this.trustCredentialsConfig.getCredentials(); + } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java index 04c6a2b160..bd08aa44eb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MAuthorizer.java @@ -28,7 +28,7 @@ import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; -import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; +import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; @Component @RequiredArgsConstructor @@ -37,7 +37,7 @@ import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; public class TbLwM2MAuthorizer implements Authorizer { private final TbLwM2MDtlsSessionStore sessionStorage; - private final TbSecurityStore securityStore; + private final TbMainSecurityStore securityStore; private final SecurityChecker securityChecker = new SecurityChecker(); private final LwM2mClientContext clientContext; @@ -58,17 +58,16 @@ public class TbLwM2MAuthorizer implements Authorizer { // If session info is not found, this may be the trusted certificate, so we still need to check all other options below. } SecurityInfo expectedSecurityInfo = null; - if (securityStore != null) { - try { - expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint()); - } catch (LwM2MAuthException e) { - log.info("Registration failed: FORBIDDEN, endpointId: [{}]", registration.getEndpoint()); - return null; - } + try { + expectedSecurityInfo = securityStore.getByEndpoint(registration.getEndpoint()); + } catch (LwM2MAuthException e) { + log.info("Registration failed: FORBIDDEN, endpointId: [{}]", registration.getEndpoint()); + return null; } if (securityChecker.checkSecurityInfo(registration.getEndpoint(), senderIdentity, expectedSecurityInfo)) { return registration; } else { + securityStore.remove(registration.getEndpoint(), registration.getId()); return null; } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java index fe04410c17..1837093e8c 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java @@ -87,12 +87,8 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer try { /* by default trust all */ X509Certificate[] trustedCertificates = new X509Certificate[0]; - if (config.getKeyStoreValue() != null) { - X509Certificate rootCAX509Cert = (X509Certificate) config.getKeyStoreValue().getCertificate(config.getRootCertificateAlias()); - if (rootCAX509Cert != null) { - trustedCertificates = new X509Certificate[1]; - trustedCertificates[0] = rootCAX509Cert; - } + if (config.getTrustSslCredentials() != null) { + trustedCertificates = config.getTrustSslCredentials().getTrustedCertificates(); } staticCertificateVerifier = new StaticCertificateVerifier(trustedCertificates); } catch (Exception e) { 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 5a02f3b194..147e20c02b 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 @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.server; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.eclipse.californium.elements.util.SslContextUtil; import org.eclipse.californium.scandium.config.DtlsConnectorConfig; import org.eclipse.californium.scandium.dtls.cipher.CipherSuite; import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder; @@ -25,23 +24,20 @@ import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder; import org.eclipse.leshan.server.californium.LeshanServer; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; -import org.eclipse.leshan.server.model.LwM2mModelProvider; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.ota.OtaPackageDataCache; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; -import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2MUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; -import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; -import java.security.PrivateKey; -import java.security.PublicKey; import java.security.cert.X509Certificate; import static org.eclipse.californium.scandium.dtls.cipher.CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256; @@ -72,7 +68,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private LeshanServer server; - @PostConstruct + @AfterStartUp public void init() { this.server = getLhServer(); /* @@ -141,7 +137,11 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { } private void setServerWithCredentials(LeshanServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) { - if (config.getKeyStoreValue() != null && this.setBuilderX509(builder)) { + if (this.config.getSslCredentials() != null) { + SslCredentials sslCredentials = this.config.getSslCredentials(); + builder.setPublicKey(sslCredentials.getPublicKey()); + builder.setPrivateKey(sslCredentials.getPrivateKey()); + builder.setCertificateChain(sslCredentials.getCertificateChain()); dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier); builder.setAuthorizer(authorizer); dtlsConfig.setSupportedCipherSuites(RPK_OR_X509_CIPHER_SUITES); @@ -153,26 +153,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { } } - private boolean setBuilderX509(LeshanServerBuilder builder) { - try { - X509Certificate[] certificateChain = SslContextUtil.asX509Certificates(config.getKeyStoreValue().getCertificateChain(config.getCertificateAlias())); - X509Certificate serverCertificate = certificateChain[0]; - PrivateKey privateKey = (PrivateKey) config.getKeyStoreValue().getKey(config.getCertificateAlias(), config.getCertificatePassword() == null ? null : config.getCertificatePassword().toCharArray()); - PublicKey publicKey = serverCertificate.getPublicKey(); - if (privateKey != null && privateKey.getEncoded().length > 0 && publicKey != null && publicKey.getEncoded().length > 0) { - builder.setPublicKey(serverCertificate.getPublicKey()); - builder.setPrivateKey(privateKey); - builder.setCertificateChain(certificateChain); - return true; - } else { - return false; - } - } catch (Exception ex) { - log.error("[{}] Unable to load KeyStore files server", ex.getMessage()); - return false; - } - } - @Override public String getName() { return DataConstants.LWM2M_TRANSPORT_NAME; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java index 4fc60aecfc..2b488a3ab9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mNetworkConfig.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.server; import org.eclipse.californium.core.network.config.NetworkConfig; import org.eclipse.californium.core.network.config.NetworkConfigDefaults; +import org.springframework.util.CollectionUtils; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import static org.eclipse.californium.core.network.config.NetworkConfigDefaults.DEFAULT_BLOCKWISE_STATUS_LIFETIME; @@ -25,8 +26,8 @@ public class LwM2mNetworkConfig { public static NetworkConfig getCoapConfig(Integer serverPortNoSec, Integer serverSecurePort, LwM2MTransportServerConfig config) { NetworkConfig coapConfig = new NetworkConfig(); - coapConfig.setInt(NetworkConfig.Keys.COAP_PORT,serverPortNoSec); - coapConfig.setInt(NetworkConfig.Keys.COAP_SECURE_PORT,serverSecurePort); + coapConfig.setInt(NetworkConfig.Keys.COAP_PORT, serverPortNoSec); + coapConfig.setInt(NetworkConfig.Keys.COAP_SECURE_PORT, serverSecurePort); /** Example:Property for large packet: #NetworkConfig config = new NetworkConfig(); @@ -105,6 +106,10 @@ public class LwM2mNetworkConfig { coapConfig.setInt(NetworkConfig.Keys.MAX_RETRANSMIT, 10); + if (!CollectionUtils.isEmpty(config.getNetworkConfig())) { + config.getNetworkConfig().forEach(p -> coapConfig.setString(p.getKey(), p.getValue())); + } + return coapConfig; } } \ No newline at end of file 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 00aeacda95..3e0ad17925 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 @@ -19,13 +19,12 @@ import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg; @@ -109,4 +108,10 @@ public class LwM2mSessionMsgListener implements GenericFutureListener lwM2mClientsByRegistrationId = new ConcurrentHashMap<>(); private final Map profiles = new ConcurrentHashMap<>(); - @AfterStartUp + @AfterStartUp(order = Integer.MAX_VALUE - 1) public void init() { String nodeId = context.getNodeId(); Set fetchedClients = clientStore.getAll(); @@ -278,8 +278,8 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { public LwM2mClient getClientBySessionInfo(TransportProtos.SessionInfoProto sessionInfo) { LwM2mClient lwM2mClient = null; UUID sessionId = new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB()); - Predicate isClientFilter = c -> - sessionId.equals((new UUID(c.getSession().getSessionIdMSB(), c.getSession().getSessionIdLSB()))); + Predicate isClientFilter = + c -> c.getSession() != null && sessionId.equals((new UUID(c.getSession().getSessionIdMSB(), c.getSession().getSessionIdLSB()))); if (this.lwM2mClientsByEndpoint.size() > 0) { lwM2mClient = this.lwM2mClientsByEndpoint.values().stream().filter(isClientFilter).findAny().orElse(null); } @@ -328,11 +328,6 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { } } - @Override - public void removeCredentials(TransportProtos.SessionInfoProto sessionInfo) { - //TODO: implement - } - @Override public void sendMsgsAfterSleeping(LwM2mClient lwM2MClient) { if (LwM2MClientState.REGISTERED.equals(lwM2MClient.getState())) { 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 64d42f001e..2ec0122c28 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 @@ -186,6 +186,9 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } sendSimpleRequest(client, downlink, request.getTimeout(), callback); } + else { + callback.onValidationError(toString(request), "Tenant hasn't such the TbResources: " + request.getVersionedId() + "!"); + } } @Override @@ -245,7 +248,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im callback.onError(toString(request), e); } } else { - callback.onValidationError(toString(request), "Resource " + request.getVersionedId() + " is not configured in the device profile!"); + callback.onValidationError(toString(request), "Tenant hasn't such the TbResources: " + request.getVersionedId() + "!"); } } @@ -271,10 +274,15 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im **/ Collection resources = client.getNewResourceForInstance(request.getVersionedId(), request.getValue(), modelProvider, this.converter); ResourceModel resourceModelWrite = client.getResourceModel(request.getVersionedId(), modelProvider); - ContentFormat contentFormat = request.getObjectContentFormat() != null ? request.getObjectContentFormat() : convertResourceModelTypeToContentFormat(client, resourceModelWrite.type); - WriteRequest downlink = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), - resultIds.getObjectInstanceId(), resources); - sendSimpleRequest(client, downlink, request.getTimeout(), callback); + if (resourceModelWrite != null) { + ContentFormat contentFormat = request.getObjectContentFormat() != null ? request.getObjectContentFormat() : convertResourceModelTypeToContentFormat(client, resourceModelWrite.type); + WriteRequest downlink = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(), + resultIds.getObjectInstanceId(), resources); + sendSimpleRequest(client, downlink, request.getTimeout(), callback); + } + else { + callback.onValidationError(toString(request), "Tenant hasn't such the TbResources: " + request.getVersionedId() + " !"); + } } else if (resultIds.isObjectInstance()) { /* * params = "{\"id\":0,\"resources\":[{\"id\":14,\"value\":\"+5\"},{\"id\":15,\"value\":\"+9\"}]}" @@ -311,6 +319,10 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im Registration registration = client.getRegistration(); try { logService.log(client, String.format("[%s][%s] Sending request: %s to %s", registration.getId(), registration.getSocketAddress(), request.getClass().getSimpleName(), pathToStringFunction.apply(request))); + if (!callback.onSent(request)) { + return; + } + context.getServer().send(registration, request, timeoutInMs, response -> { executor.submit(() -> { try { @@ -322,7 +334,6 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im } }); }, e -> handleDownlinkError(client, request, callback, e)); - callback.onSent(request); } catch (Exception e) { handleDownlinkError(client, request, callback, e); } @@ -358,6 +369,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private , T extends LwM2mResponse> void handleDownlinkError(LwM2mClient client, R request, DownlinkRequestCallback callback, Exception e) { log.trace("[{}] Received downlink error: {}.", client.getEndpoint(), e); + client.updateLastUplinkTime(); executor.submit(() -> { if (e instanceof TimeoutException || e instanceof ClientSleepingException) { log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java index 2dc99479c1..8912c51960 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DownlinkRequestCallback.java @@ -17,7 +17,9 @@ package org.thingsboard.server.transport.lwm2m.server.downlink; public interface DownlinkRequestCallback { - default void onSent(R request){}; + default boolean onSent(R request){ + return true; + }; void onSuccess(R request, T response); 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 bdc5dcc819..a728a0b6e0 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 @@ -17,15 +17,12 @@ package org.thingsboard.server.transport.lwm2m.server.rpc; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.eclipse.leshan.core.ResponseCode; -import org.eclipse.leshan.core.request.ReadCompositeRequest; -import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; @@ -63,11 +60,9 @@ import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcReadRespon import org.thingsboard.server.transport.lwm2m.server.rpc.composite.RpcWriteCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; -import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; @Slf4j @Service @@ -85,85 +80,102 @@ public class DefaultLwM2MRpcRequestHandler implements LwM2MRpcRequestHandler { @Override public void onToDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { log.debug("Received params: {}", rpcRequest.getParams()); - LwM2mOperationType operationType = LwM2mOperationType.fromType(rpcRequest.getMethodName()); - if (operationType == null) { - this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.METHOD_NOT_ALLOWED, "Unsupported operation type: " + rpcRequest.getMethodName()); - return; - } - LwM2mClient client = clientContext.getClientBySessionInfo(sessionInfo); - if (client.getRegistration() == null) { - this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.INTERNAL_SERVER_ERROR, "Registration is empty"); - return; - } try { - if (operationType.isHasObjectId()) { - String objectId = getIdFromParameters(client, rpcRequest); - switch (operationType) { - case READ: - sendReadRequest(client, rpcRequest, objectId); - break; - case OBSERVE: - sendObserveRequest(client, rpcRequest, objectId); - break; - case DISCOVER: - sendDiscoverRequest(client, rpcRequest, objectId); - break; - case EXECUTE: - sendExecuteRequest(client, rpcRequest, objectId); - break; - case WRITE_ATTRIBUTES: - sendWriteAttributesRequest(client, rpcRequest, objectId); - break; - case OBSERVE_CANCEL: - sendCancelObserveRequest(client, rpcRequest, objectId); - break; - case DELETE: - sendDeleteRequest(client, rpcRequest, objectId); - break; - case WRITE_UPDATE: - sendWriteUpdateRequest(client, rpcRequest, objectId); - break; - case WRITE_REPLACE: - sendWriteReplaceRequest(client, rpcRequest, objectId); - break; - default: - throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); - } - } else if (operationType.isComposite()) { - if (clientContext.isComposite(client)) { + LwM2mOperationType operationType = LwM2mOperationType.fromType(rpcRequest.getMethodName()); + if (operationType == null) { + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.METHOD_NOT_ALLOWED, "Unsupported operation type: " + rpcRequest.getMethodName()); + return; + } + LwM2mClient client = clientContext.getClientBySessionInfo(sessionInfo); + + if (client == null) { + log.warn("Missing client for session: [{}]", sessionInfo); + return; + } + + if (client.getRegistration() == null) { + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.INTERNAL_SERVER_ERROR, "Registration is empty"); + return; + } + UUID rpcId = new UUID(rpcRequest.getRequestIdMSB(), rpcRequest.getRequestIdLSB()); + + if (rpcId.equals(client.getLastSentRpcId())) { + log.debug("[{}]][{}] Rpc has already sent!", client.getEndpoint(), rpcId); + return; + } + try { + if (operationType.isHasObjectId()) { + String objectId = getIdFromParameters(client, rpcRequest); switch (operationType) { - case READ_COMPOSITE: - sendReadCompositeRequest(client, rpcRequest); + case READ: + sendReadRequest(client, rpcRequest, objectId); + break; + case OBSERVE: + sendObserveRequest(client, rpcRequest, objectId); + break; + case DISCOVER: + sendDiscoverRequest(client, rpcRequest, objectId); + break; + case EXECUTE: + sendExecuteRequest(client, rpcRequest, objectId); break; - case WRITE_COMPOSITE: - sendWriteCompositeRequest(client, rpcRequest); + case WRITE_ATTRIBUTES: + sendWriteAttributesRequest(client, rpcRequest, objectId); + break; + case OBSERVE_CANCEL: + sendCancelObserveRequest(client, rpcRequest, objectId); + break; + case DELETE: + sendDeleteRequest(client, rpcRequest, objectId); + break; + case WRITE_UPDATE: + sendWriteUpdateRequest(client, rpcRequest, objectId); + break; + case WRITE_REPLACE: + sendWriteReplaceRequest(client, rpcRequest, objectId); break; default: throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); } + } else if (operationType.isComposite()) { + if (clientContext.isComposite(client)) { + switch (operationType) { + case READ_COMPOSITE: + sendReadCompositeRequest(client, rpcRequest); + break; + case WRITE_COMPOSITE: + sendWriteCompositeRequest(client, rpcRequest); + break; + default: + throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); + } + } else { + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ResponseCode.INTERNAL_SERVER_ERROR, "This device does not support Composite Operation"); + } } else { - this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), - ResponseCode.INTERNAL_SERVER_ERROR, "This device does not support Composite Operation"); - } - } else { - switch (operationType) { - case OBSERVE_CANCEL_ALL: - sendCancelAllObserveRequest(client, rpcRequest); - break; - case OBSERVE_READ_ALL: - sendObserveAllRequest(client, rpcRequest); - break; - case DISCOVER_ALL: - sendDiscoverAllRequest(client, rpcRequest); - break; - case FW_UPDATE: - //TODO: implement and add break statement - default: - throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); + switch (operationType) { + case OBSERVE_CANCEL_ALL: + sendCancelAllObserveRequest(client, rpcRequest); + break; + case OBSERVE_READ_ALL: + sendObserveAllRequest(client, rpcRequest); + break; + case DISCOVER_ALL: + sendDiscoverAllRequest(client, rpcRequest); + break; + case FW_UPDATE: + //TODO: implement and add break statement + default: + throw new IllegalArgumentException("Unsupported operation: " + operationType.name()); + } } + } catch (IllegalArgumentException e) { + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.BAD_REQUEST, e.getMessage()); } - } catch (IllegalArgumentException e) { - this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.BAD_REQUEST, e.getMessage()); + } catch (Exception e) { + log.error("[{}] Failed to send RPC: [{}]", sessionInfo, rpcRequest, e); + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.INTERNAL_SERVER_ERROR, ExceptionUtils.getRootCauseMessage(e)); } } 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 5361682491..6221a68149 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 @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.server.rpc; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.request.exception.ClientSleepingException; import org.thingsboard.common.util.JacksonUtil; @@ -26,8 +27,10 @@ import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; +import java.util.UUID; import java.util.concurrent.TimeoutException; +@Slf4j public abstract class RpcDownlinkRequestCallbackProxy implements DownlinkRequestCallback { private final TransportService transportService; @@ -44,8 +47,20 @@ public abstract class RpcDownlinkRequestCallbackProxy implements DownlinkR } @Override - public void onSent(R request) { + public boolean onSent(R request) { + client.lock(); + try { + UUID rpcId = new UUID(this.request.getRequestIdMSB(), this.request.getRequestIdLSB()); + if (rpcId.equals(client.getLastSentRpcId())) { + log.debug("[{}]][{}] Rpc has already sent!", client.getEndpoint(), rpcId); + return false; + } + client.setLastSentRpcId(rpcId); + } finally { + client.unlock(); + } transportService.process(client.getSession(), this.request, RpcStatus.SENT, TransportServiceCallback.EMPTY); + return true; } @Override @@ -68,6 +83,7 @@ public abstract class RpcDownlinkRequestCallbackProxy implements DownlinkR @Override public void onError(String params, Exception e) { if (e instanceof TimeoutException || e instanceof org.eclipse.leshan.core.request.exception.TimeoutException) { + client.setLastSentRpcId(null); 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 cc0a436746..3a24a19d9b 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 @@ -36,6 +36,7 @@ import org.eclipse.leshan.core.response.ObserveResponse; import org.eclipse.leshan.core.response.ReadCompositeResponse; import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.server.registration.Registration; +import org.eclipse.leshan.server.registration.RegistrationStore; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.DonAsynchron; @@ -46,6 +47,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.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageUtil; import org.thingsboard.server.common.transport.TransportService; @@ -82,9 +84,9 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttrib import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; -import org.thingsboard.server.transport.lwm2m.server.rpc.LwM2MRpcRequestHandler; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; +import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mSecurityStore; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PostConstruct; @@ -143,6 +145,8 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl private final LwM2mClientContext clientContext; private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; private final LwM2mVersionedModelProvider modelProvider; + private final RegistrationStore registrationStore; + private final TbLwM2mSecurityStore securityStore; public DefaultLwM2MUplinkMsgHandler(TransportService transportService, LwM2MTransportServerConfig config, @@ -155,7 +159,9 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl @Lazy LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler, LwM2mTransportContext context, TbLwM2MDtlsSessionStore sessionStore, - LwM2mVersionedModelProvider modelProvider) { + LwM2mVersionedModelProvider modelProvider, + RegistrationStore registrationStore, + TbLwM2mSecurityStore securityStore) { this.transportService = transportService; this.sessionManager = sessionManager; this.attributesService = attributesService; @@ -168,6 +174,8 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl this.context = context; this.sessionStore = sessionStore; this.modelProvider = modelProvider; + this.registrationStore = registrationStore; + this.securityStore = securityStore; } @PostConstruct @@ -273,26 +281,27 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl * @param observations - !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect */ public void unReg(Registration registration, Collection observations) { - executor.submit(() -> { - LwM2mClient client = clientContext.getClientByEndpoint(registration.getEndpoint()); - try { - logService.log(client, LOG_LWM2M_INFO + ": Client unRegistration"); - clientContext.unregister(client, registration); - SessionInfoProto sessionInfo = client.getSession(); - if (sessionInfo != null) { - sessionManager.deregister(sessionInfo); - sessionStore.remove(registration.getEndpoint()); - log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); - } else { - log.error("Client close session: [{}] unReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); - } - } catch (LwM2MClientStateException stateException) { - log.info("[{}] delete registration: [{}] {}.", registration.getEndpoint(), stateException.getState(), stateException.getMessage()); - } catch (Throwable t) { - log.error("[{}] endpoint [{}] error Unable un registration.", registration.getEndpoint(), t); - logService.log(client, LOG_LWM2M_ERROR + String.format(": Client Unable un Registration, %s", t.getMessage())); + executor.submit(() -> doUnReg(registration, clientContext.getClientByEndpoint(registration.getEndpoint()))); + } + + private void doUnReg(Registration registration, LwM2mClient client) { + try { + logService.log(client, LOG_LWM2M_INFO + ": Client unRegistration"); + clientContext.unregister(client, registration); + SessionInfoProto sessionInfo = client.getSession(); + if (sessionInfo != null) { + sessionManager.deregister(sessionInfo); + sessionStore.remove(registration.getEndpoint()); + log.info("Client close session: [{}] unReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); + } else { + log.error("Client close session: [{}] unReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null); } - }); + } catch (LwM2MClientStateException stateException) { + log.info("[{}] delete registration: [{}] {}.", registration.getEndpoint(), stateException.getState(), stateException.getMessage()); + } catch (Throwable t) { + log.error("[{}] endpoint [{}] error Unable un registration.", registration.getEndpoint(), t); + logService.log(client, LOG_LWM2M_ERROR + String.format(": Client Unable un Registration, %s", t.getMessage())); + } } @Override @@ -369,9 +378,13 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl List clients = clientContext.getLwM2mClients() .stream().filter(e -> e.getProfileId() != null) .filter(e -> e.getProfileId().equals(deviceProfile.getUuidId())).collect(Collectors.toList()); - clients.forEach(client -> client.onDeviceProfileUpdate(deviceProfile)); + clients.forEach(client -> { + this.securityStore.remove(client.getEndpoint(), client.getRegistration().getId()); + client.onDeviceProfileUpdate(deviceProfile); + }); if (clients.size() > 0) { - this.onDeviceProfileUpdate(clients, deviceProfile); + var oldProfile = clientContext.getProfile(deviceProfile.getUuidId()); + this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile); } } catch (Exception e) { log.warn("[{}] failed to update profile: {}", deviceProfile.getId(), deviceProfile); @@ -379,17 +392,25 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } @Override - public void onDeviceUpdate(SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) { + public void onDeviceUpdate(SessionInfoProto sessionInfo, Device device, Optional newDeviceProfileOpt) { try { LwM2mClient client = clientContext.getClientByDeviceId(device.getUuidId()); if (client != null) { - this.onDeviceUpdate(client, device, deviceProfileOpt); + if (newDeviceProfileOpt.isPresent()) { + this.securityStore.remove(client.getEndpoint(), client.getRegistration().getId()); + } + this.onDeviceUpdate(client, device, newDeviceProfileOpt); } } catch (Exception e) { log.warn("[{}] failed to update device: {}", device.getId(), device); } } + @Override + public void onDeviceDelete(DeviceId deviceId) { + clearAndUnregister(clientContext.getClientByDeviceId(deviceId.getId())); + } + @Override public void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt) { String idVer = resourceUpdateMsgOpt.getResourceKey(); @@ -631,7 +652,8 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } private void onDeviceUpdate(LwM2mClient lwM2MClient, Device device, Optional deviceProfileOpt) { - deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), deviceProfile)); + var oldProfile = clientContext.getProfile(lwM2MClient.getProfileId()); + deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile)); lwM2MClient.onDeviceUpdate(device, deviceProfileOpt); } @@ -740,8 +762,7 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl } //TODO: review and optimize the logic to minimize number of the requests to device. - private void onDeviceProfileUpdate(List clients, DeviceProfile deviceProfile) { - var oldProfile = clientContext.getProfile(deviceProfile.getUuidId()); + private void onDeviceProfileUpdate(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfile, DeviceProfile deviceProfile) { if (clientContext.profileUpdate(deviceProfile) != null) { // #1 TelemetryMappingConfiguration oldTelemetryParams = oldProfile.getObserveAttr(); @@ -886,8 +907,8 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl */ @Override public void onToTransportUpdateCredentials(SessionInfoProto sessionInfo, TransportProtos.ToTransportUpdateCredentialsProto updateCredentials) { - log.info("[{}] idList [{}] valueList updateCredentials", updateCredentials.getCredentialsIdList(), updateCredentials.getCredentialsValueList()); - this.clientContext.removeCredentials(sessionInfo); + log.info("[{}] updateCredentials", sessionInfo); + clearAndUnregister(clientContext.getClientBySessionInfo(sessionInfo)); } /** @@ -964,4 +985,16 @@ public class DefaultLwM2MUplinkMsgHandler extends LwM2MExecutorAwareService impl .setLastActivityTime(System.currentTimeMillis()) .build(), TransportServiceCallback.EMPTY); } + + private void clearAndUnregister(LwM2mClient client) { + client.lock(); + try { + Registration registration = client.getRegistration(); + doUnReg(registration, client); + securityStore.remove(registration.getEndpoint(), registration.getId()); + registrationStore.removeRegistration(registration.getId()); + } finally { + client.unlock(); + } + } } 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 57490e76a3..24045451f8 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 @@ -23,6 +23,7 @@ import org.eclipse.leshan.core.response.ReadResponse; import org.eclipse.leshan.server.registration.Registration; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; @@ -48,6 +49,8 @@ public interface LwM2mUplinkMsgHandler { void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt); + void onDeviceDelete(DeviceId deviceId); + void onResourceUpdate(TransportProtos.ResourceUpdateMsg resourceUpdateMsgOpt); void onResourceDelete(TransportProtos.ResourceDeleteMsg resourceDeleteMsgOpt); diff --git a/common/transport/lwm2m/src/main/resources/lwm2mserver.jks b/common/transport/lwm2m/src/main/resources/lwm2mserver.jks index 9f6748f8fd..5fab824aa1 100644 Binary files a/common/transport/lwm2m/src/main/resources/lwm2mserver.jks and b/common/transport/lwm2m/src/main/resources/lwm2mserver.jks differ diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 5d93b95615..0136730eb3 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index fad743aec2..215aef408c 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -18,16 +18,20 @@ package org.thingsboard.server.transport.mqtt; import io.netty.handler.ssl.SslHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.ResourceUtils; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.config.ssl.SslCredentials; +import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig; import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; @@ -38,8 +42,6 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import java.io.InputStream; -import java.security.KeyStore; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -56,18 +58,20 @@ public class MqttSslHandlerProvider { @Value("${transport.mqtt.ssl.protocol}") private String sslProtocol; - @Value("${transport.mqtt.ssl.key_store}") - private String keyStoreFile; - @Value("${transport.mqtt.ssl.key_store_password}") - private String keyStorePassword; - @Value("${transport.mqtt.ssl.key_password}") - private String keyPassword; - @Value("${transport.mqtt.ssl.key_store_type}") - private String keyStoreType; @Autowired private TransportService transportService; + @Bean + @ConfigurationProperties(prefix = "transport.mqtt.ssl.credentials") + public SslCredentialsConfig mqttSslCredentials() { + return new SslCredentialsConfig("MQTT SSL Credentials", false); + } + + @Autowired + @Qualifier("mqttSslCredentials") + private SslCredentialsConfig mqttSslCredentialsConfig; + private SSLContext sslContext; public SslHandler getSslHandler() { @@ -86,19 +90,9 @@ public class MqttSslHandlerProvider { private SSLContext createSslContext() { try { - TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - KeyStore trustStore = KeyStore.getInstance(keyStoreType); - try (InputStream tsFileInputStream = ResourceUtils.getInputStream(this, keyStoreFile)) { - trustStore.load(tsFileInputStream, keyStorePassword.toCharArray()); - } - tmFactory.init(trustStore); - - KeyStore ks = KeyStore.getInstance(keyStoreType); - try (InputStream ksFileInputStream = ResourceUtils.getInputStream(this, keyStoreFile)) { - ks.load(ksFileInputStream, keyStorePassword.toCharArray()); - } - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(ks, keyPassword.toCharArray()); + SslCredentials sslCredentials = this.mqttSslCredentialsConfig.getCredentials(); + TrustManagerFactory tmFactory = sslCredentials.createTrustManagerFactory(); + KeyManagerFactory kmf = sslCredentials.createKeyManagerFactory(); KeyManager[] km = kmf.getKeyManagers(); TrustManager x509wrapped = getX509TrustManager(tmFactory); 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 35e6e8c508..693b47d42f 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 @@ -951,7 +951,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onError(Throwable e) { if (e instanceof TbRateLimitsException) { - log.trace("[{}] Failed to submit session event", sessionId, e); + log.trace("[{}] Failed to submit session event: {}", sessionId, e.getMessage()); } else { log.warn("[{}] Failed to submit session event", sessionId, e); } @@ -965,26 +965,8 @@ 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; - } + String topicBase = attrReqTopicType.getAttributesResponseTopicBase(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrReqTopicType); try { adaptor.convertToPublish(deviceSessionCtx, response, topicBase).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { @@ -995,27 +977,8 @@ 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; - } + String topic = attrSubTopicType.getAttributesSubTopic(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType); try { adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { @@ -1031,27 +994,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { - 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; - } + log.trace("[{}] Received RPC command to device", sessionId); + String baseTopic = rpcSubTopicType.getRpcRequestTopicBase(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType); try { adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic).ifPresent(payload -> { int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); @@ -1088,26 +1033,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { 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; - } + String baseTopic = toServerRpcSubTopicType.getRpcResponseTopicBase(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(toServerRpcSubTopicType); try { adaptor.convertToPublish(deviceSessionCtx, rpcResponse, baseTopic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); } catch (Exception e) { @@ -1133,8 +1060,4 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement 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/TopicType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TopicType.java new file mode 100644 index 0000000000..bd2e59a0df --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TopicType.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.transport.mqtt; + +import lombok.Getter; +import org.thingsboard.server.common.data.device.profile.MqttTopics; + +public enum TopicType { + + V1(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_TOPIC), + V2(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_TOPIC), + V2_JSON(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_JSON_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_JSON_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_JSON_TOPIC), + V2_PROTO(MqttTopics.DEVICE_ATTRIBUTES_RESPONSE_SHORT_PROTO_TOPIC_PREFIX, MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_RPC_REQUESTS_SHORT_PROTO_TOPIC, MqttTopics.DEVICE_RPC_RESPONSE_SHORT_PROTO_TOPIC); + + @Getter + private final String attributesResponseTopicBase; + + @Getter + private final String attributesSubTopic; + + @Getter + private final String rpcRequestTopicBase; + + @Getter + private final String rpcResponseTopicBase; + + TopicType(String attributesRequestTopicBase, String attributesSubTopic, String rpcRequestTopicBase, String rpcResponseTopicBase) { + this.attributesResponseTopicBase = attributesRequestTopicBase; + this.attributesSubTopic = attributesSubTopic; + this.rpcRequestTopicBase = rpcRequestTopicBase; + this.rpcResponseTopicBase = rpcResponseTopicBase; + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java new file mode 100644 index 0000000000..a608d6c52d --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/BackwardCompatibilityAdaptor.java @@ -0,0 +1,153 @@ +/** + * 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.transport.mqtt.adaptors; + +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.ota.OtaPackageType; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionContext; + +import java.util.Optional; + +@Data +@AllArgsConstructor +@Slf4j +public class BackwardCompatibilityAdaptor implements MqttTransportAdaptor { + + private MqttTransportAdaptor protoAdaptor; + private MqttTransportAdaptor jsonAdaptor; + + @Override + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + try { + return protoAdaptor.convertToPostTelemetry(ctx, inbound); + } catch (AdaptorException e) { + log.trace("[{}] failed to process post telemetry request msg: {} due to: ", ctx.getSessionId(), inbound, e); + return jsonAdaptor.convertToPostTelemetry(ctx, inbound); + } + } + + @Override + public TransportProtos.PostAttributeMsg convertToPostAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + try { + return protoAdaptor.convertToPostAttributes(ctx, inbound); + } catch (AdaptorException e) { + log.trace("[{}] failed to process post attributes request msg: {} due to: ", ctx.getSessionId(), inbound, e); + return jsonAdaptor.convertToPostAttributes(ctx, inbound); + } + } + + @Override + public TransportProtos.GetAttributeRequestMsg convertToGetAttributes(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound, String topicBase) throws AdaptorException { + try { + return protoAdaptor.convertToGetAttributes(ctx, inbound, topicBase); + } catch (AdaptorException e) { + log.trace("[{}] failed to process get attributes request msg: {} due to: ", ctx.getSessionId(), inbound, e); + return jsonAdaptor.convertToGetAttributes(ctx, inbound, topicBase); + } + } + + @Override + public TransportProtos.ToDeviceRpcResponseMsg convertToDeviceRpcResponse(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { + try { + return protoAdaptor.convertToDeviceRpcResponse(ctx, mqttMsg, topicBase); + } catch (AdaptorException e) { + log.trace("[{}] failed to process to device rpc response msg: {} due to: ", ctx.getSessionId(), mqttMsg, e); + return jsonAdaptor.convertToDeviceRpcResponse(ctx, mqttMsg, topicBase); + } + } + + @Override + public TransportProtos.ToServerRpcRequestMsg convertToServerRpcRequest(MqttDeviceAwareSessionContext ctx, MqttPublishMessage mqttMsg, String topicBase) throws AdaptorException { + try { + return protoAdaptor.convertToServerRpcRequest(ctx, mqttMsg, topicBase); + } catch (AdaptorException e) { + log.trace("[{}] failed to process to server rpc request msg: {} due to: ", ctx.getSessionId(), mqttMsg, e); + return jsonAdaptor.convertToServerRpcRequest(ctx, mqttMsg, topicBase); + } + } + + @Override + public TransportProtos.ClaimDeviceMsg convertToClaimDevice(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + try { + return protoAdaptor.convertToClaimDevice(ctx, inbound); + } catch (AdaptorException e) { + log.trace("[{}] failed to process claim device request msg: {} due to: ", ctx.getSessionId(), inbound, e); + return jsonAdaptor.convertToClaimDevice(ctx, inbound); + } + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.GetAttributeResponseMsg responseMsg, String topicBase) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! GetAttributeResponseMsg: {} TopicBase: {}", ctx.getSessionId(), responseMsg, topicBase); + return Optional.empty(); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { + return protoAdaptor.convertToGatewayPublish(ctx, deviceName, responseMsg); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! AttributeUpdateNotificationMsg: {} Topic: {}", ctx.getSessionId(), notificationMsg, topic); + return Optional.empty(); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.AttributeUpdateNotificationMsg notificationMsg) throws AdaptorException { + return protoAdaptor.convertToGatewayPublish(ctx, deviceName, notificationMsg); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, String topicBase) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! ToDeviceRpcRequestMsg: {} TopicBase: {}", ctx.getSessionId(), rpcRequest, topicBase); + return Optional.empty(); + } + + @Override + public Optional convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws AdaptorException { + return protoAdaptor.convertToGatewayPublish(ctx, deviceName, rpcRequest); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ToServerRpcResponseMsg rpcResponse, String topicBase) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! ToServerRpcResponseMsg: {} TopicBase: {}", ctx.getSessionId(), rpcResponse, topicBase); + return Optional.empty(); + } + + @Override + public TransportProtos.ProvisionDeviceRequestMsg convertToProvisionRequestMsg(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! MqttPublishMessage: {}", ctx.getSessionId(), inbound); + return null; + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.ProvisionDeviceResponseMsg provisionResponse) throws AdaptorException { + log.warn("[{}] invoked not implemented adaptor method! ProvisionDeviceResponseMsg: {}", ctx.getSessionId(), provisionResponse); + return Optional.empty(); + } + + @Override + public Optional convertToPublish(MqttDeviceAwareSessionContext ctx, byte[] firmwareChunk, String requestId, int chunk, OtaPackageType firmwareType) throws AdaptorException { + return protoAdaptor.convertToPublish(ctx, firmwareChunk, requestId, chunk, firmwareType); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java index ab11d859b8..9f0c89714a 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java @@ -32,13 +32,14 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.mqtt.MqttTransportContext; +import org.thingsboard.server.transport.mqtt.TopicType; +import org.thingsboard.server.transport.mqtt.adaptors.BackwardCompatibilityAdaptor; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; import java.util.Collection; import java.util.Collections; -import java.util.Queue; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentMap; @@ -80,6 +81,9 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor; private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor; private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder; + private volatile MqttTransportAdaptor adaptor; + private volatile boolean jsonPayloadFormatCompatibilityEnabled; + private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics; @Getter @Setter @@ -88,6 +92,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public DeviceSessionCtx(UUID sessionId, ConcurrentMap mqttQoSMap, MqttTransportContext context) { super(sessionId, mqttQoSMap); this.context = context; + this.adaptor = context.getJsonMqttAdaptor(); } public int nextMsgId() { @@ -103,7 +108,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { } public MqttTransportAdaptor getPayloadAdaptor() { - return payloadType.equals(TransportPayloadType.JSON) ? context.getJsonMqttAdaptor() : context.getProtoMqttAdaptor(); + return adaptor; } public boolean isJsonPayloadType() { @@ -129,16 +134,16 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { @Override public void setDeviceProfile(DeviceProfile deviceProfile) { super.setDeviceProfile(deviceProfile); - updateTopicFilters(deviceProfile); + updateDeviceSessionConfiguration(deviceProfile); } @Override public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { super.onDeviceProfileUpdate(sessionInfo, deviceProfile); - updateTopicFilters(deviceProfile); + updateDeviceSessionConfiguration(deviceProfile); } - private void updateTopicFilters(DeviceProfile deviceProfile) { + private void updateDeviceSessionConfiguration(DeviceProfile deviceProfile) { DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) && transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { @@ -148,22 +153,57 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { telemetryTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceTelemetryTopic()); attributesTopicFilter = MqttTopicFilterFactory.toFilter(mqttConfig.getDeviceAttributesTopic()); if (TransportPayloadType.PROTOBUF.equals(payloadType)) { - updateDynamicMessageDescriptors(transportPayloadTypeConfiguration); + ProtoTransportPayloadConfiguration protoTransportPayloadConfig = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + updateDynamicMessageDescriptors(protoTransportPayloadConfig); + jsonPayloadFormatCompatibilityEnabled = protoTransportPayloadConfig.isEnableCompatibilityWithJsonPayloadFormat(); + useJsonPayloadFormatForDefaultDownlinkTopics = jsonPayloadFormatCompatibilityEnabled && protoTransportPayloadConfig.isUseJsonPayloadFormatForDefaultDownlinkTopics(); } } else { telemetryTopicFilter = MqttTopicFilterFactory.getDefaultTelemetryFilter(); attributesTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); + payloadType = TransportPayloadType.JSON; } + updateAdaptor(); } - private void updateDynamicMessageDescriptors(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration) { - ProtoTransportPayloadConfiguration protoTransportPayloadConfig = (ProtoTransportPayloadConfiguration) transportPayloadTypeConfiguration; + private void updateDynamicMessageDescriptors(ProtoTransportPayloadConfiguration protoTransportPayloadConfig) { telemetryDynamicMessageDescriptor = protoTransportPayloadConfig.getTelemetryDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceTelemetryProtoSchema()); attributesDynamicMessageDescriptor = protoTransportPayloadConfig.getAttributesDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceAttributesProtoSchema()); rpcResponseDynamicMessageDescriptor = protoTransportPayloadConfig.getRpcResponseDynamicMessageDescriptor(protoTransportPayloadConfig.getDeviceRpcResponseProtoSchema()); rpcRequestDynamicMessageBuilder = protoTransportPayloadConfig.getRpcRequestDynamicMessageBuilder(protoTransportPayloadConfig.getDeviceRpcRequestProtoSchema()); } + public MqttTransportAdaptor getAdaptor(TopicType topicType) { + switch (topicType) { + case V2: + return getDefaultAdaptor(); + case V2_JSON: + return context.getJsonMqttAdaptor(); + case V2_PROTO: + return context.getProtoMqttAdaptor(); + default: + return useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor(); + } + } + + private MqttTransportAdaptor getDefaultAdaptor() { + return isJsonPayloadType() ? context.getJsonMqttAdaptor() : context.getProtoMqttAdaptor(); + } + + private void updateAdaptor() { + if (isJsonPayloadType()) { + adaptor = context.getJsonMqttAdaptor(); + jsonPayloadFormatCompatibilityEnabled = false; + useJsonPayloadFormatForDefaultDownlinkTopics = false; + } else { + if (jsonPayloadFormatCompatibilityEnabled) { + adaptor = new BackwardCompatibilityAdaptor(context.getProtoMqttAdaptor(), context.getJsonMqttAdaptor()); + } else { + adaptor = context.getProtoMqttAdaptor(); + } + } + } + public void addToQueue(MqttMessage msg) { msgQueueSize.incrementAndGet(); ReferenceCountUtil.retain(msg); diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 113e7b46a9..d82fac0264 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 12576e9a54..7b3535db82 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index fad019a1b5..df14a28436 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.common.transport @@ -129,6 +129,14 @@ org.eclipse.leshan leshan-server-cf + + org.bouncycastle + bcprov-jdk15on + + + org.bouncycastle + bcpkix-jdk15on + diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java new file mode 100644 index 0000000000..792d46b475 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java @@ -0,0 +1,195 @@ +/** + * 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.transport.config.ssl; + +import org.thingsboard.server.common.data.StringUtils; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableEntryException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +public abstract class AbstractSslCredentials implements SslCredentials { + + private char[] keyPasswordArray; + + private KeyStore keyStore; + + private PrivateKey privateKey; + + private PublicKey publicKey; + + private X509Certificate[] chain; + + private X509Certificate[] trusts; + + @Override + public void init(boolean trustsOnly) throws IOException, GeneralSecurityException { + String keyPassword = getKeyPassword(); + if (StringUtils.isEmpty(keyPassword)) { + this.keyPasswordArray = new char[0]; + } else { + this.keyPasswordArray = keyPassword.toCharArray(); + } + this.keyStore = this.loadKeyStore(trustsOnly, this.keyPasswordArray); + Set trustedCerts = getTrustedCerts(this.keyStore); + this.trusts = trustedCerts.toArray(new X509Certificate[0]); + if (!trustsOnly) { + PrivateKeyEntry privateKeyEntry = null; + String keyAlias = this.getKeyAlias(); + if (!StringUtils.isEmpty(keyAlias)) { + privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, keyAlias, this.keyPasswordArray); + } else { + for (Enumeration e = this.keyStore.aliases(); e.hasMoreElements(); ) { + String alias = e.nextElement(); + privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, alias, this.keyPasswordArray); + if (privateKeyEntry != null) { + this.updateKeyAlias(alias); + break; + } + } + } + if (privateKeyEntry == null) { + throw new IllegalArgumentException("Failed to get private key from the keystore or pem files. " + + "Please check if the private key exists in the keystore or pem files and if the provided private key password is valid."); + } + this.chain = asX509Certificates(privateKeyEntry.getCertificateChain()); + this.privateKey = privateKeyEntry.getPrivateKey(); + if (this.chain.length > 0) { + this.publicKey = this.chain[0].getPublicKey(); + } + } + } + + @Override + public KeyStore getKeyStore() { + return this.keyStore; + } + + @Override + public PrivateKey getPrivateKey() { + return this.privateKey; + } + + @Override + public PublicKey getPublicKey() { + return this.publicKey; + } + + @Override + public X509Certificate[] getCertificateChain() { + return this.chain; + } + + @Override + public X509Certificate[] getTrustedCertificates() { + return this.trusts; + } + + @Override + public TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException { + TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmFactory.init(this.keyStore); + return tmFactory; + } + + @Override + public KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(this.keyStore, this.keyPasswordArray); + return kmf; + } + + protected abstract boolean canUse(); + + protected abstract KeyStore loadKeyStore(boolean isPrivateKeyRequired, char[] keyPasswordArray) throws IOException, GeneralSecurityException; + + protected abstract void updateKeyAlias(String keyAlias); + + private static X509Certificate[] asX509Certificates(Certificate[] certificates) { + if (null == certificates || 0 == certificates.length) { + throw new IllegalArgumentException("certificates missing!"); + } + X509Certificate[] x509Certificates = new X509Certificate[certificates.length]; + for (int index = 0; certificates.length > index; ++index) { + if (null == certificates[index]) { + throw new IllegalArgumentException("[" + index + "] is null!"); + } + try { + x509Certificates[index] = (X509Certificate) certificates[index]; + } catch (ClassCastException e) { + throw new IllegalArgumentException("[" + index + "] is not a x509 certificate! Instead it's a " + + certificates[index].getClass().getName()); + } + } + return x509Certificates; + } + + private static PrivateKeyEntry tryGetPrivateKeyEntry(KeyStore keyStore, String alias, char[] pwd) { + PrivateKeyEntry entry = null; + try { + if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) { + try { + entry = (KeyStore.PrivateKeyEntry) keyStore + .getEntry(alias, new KeyStore.PasswordProtection(pwd)); + } catch (UnsupportedOperationException e) { + PrivateKey key = (PrivateKey) keyStore.getKey(alias, pwd); + Certificate[] certs = keyStore.getCertificateChain(alias); + entry = new KeyStore.PrivateKeyEntry(key, certs); + } + } + } catch (KeyStoreException | UnrecoverableEntryException | NoSuchAlgorithmException ignored) {} + return entry; + } + + private static Set getTrustedCerts(KeyStore ks) { + Set set = new HashSet<>(); + try { + for (Enumeration e = ks.aliases(); e.hasMoreElements(); ) { + String alias = e.nextElement(); + if (ks.isCertificateEntry(alias)) { + Certificate cert = ks.getCertificate(alias); + if (cert instanceof X509Certificate) { + set.add((X509Certificate)cert); + } + } else if (ks.isKeyEntry(alias)) { + Certificate[] certs = ks.getCertificateChain(alias); + if ((certs != null) && (certs.length > 0) && + (certs[0] instanceof X509Certificate)) { + set.add((X509Certificate)certs[0]); + } + } + } + } catch (KeyStoreException ignored) {} + return Collections.unmodifiableSet(set); + } + + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java new file mode 100644 index 0000000000..a6820d1cd8 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java @@ -0,0 +1,57 @@ +/** + * 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.transport.config.ssl; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.ResourceUtils; +import org.thingsboard.server.common.data.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +@Data +@EqualsAndHashCode(callSuper = false) +public class KeystoreSslCredentials extends AbstractSslCredentials { + + private String type; + private String storeFile; + private String storePassword; + private String keyPassword; + private String keyAlias; + + @Override + protected boolean canUse() { + return ResourceUtils.resourceExists(this, this.storeFile); + } + + @Override + protected KeyStore loadKeyStore(boolean trustsOnly, char[] keyPasswordArray) throws IOException, GeneralSecurityException { + String keyStoreType = StringUtils.isEmpty(this.type) ? KeyStore.getDefaultType() : this.type; + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + try (InputStream tsFileInputStream = ResourceUtils.getInputStream(this, this.storeFile)) { + keyStore.load(tsFileInputStream, StringUtils.isEmpty(this.storePassword) ? new char[0] : this.storePassword.toCharArray()); + } + return keyStore; + } + + @Override + protected void updateKeyAlias(String keyAlias) { + this.keyAlias = keyAlias; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java new file mode 100644 index 0000000000..e3d808f835 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java @@ -0,0 +1,143 @@ +/** + * 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.transport.config.ssl; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMDecryptorProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; +import org.thingsboard.server.common.data.ResourceUtils; +import org.thingsboard.server.common.data.StringUtils; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.CertPath; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@Data +@EqualsAndHashCode(callSuper = false) +public class PemSslCredentials extends AbstractSslCredentials { + + private static final String DEFAULT_KEY_ALIAS = "server"; + + private String certFile; + private String keyFile; + private String keyPassword; + + @Override + protected boolean canUse() { + return ResourceUtils.resourceExists(this, this.certFile); + } + + @Override + protected KeyStore loadKeyStore(boolean trustsOnly, char[] keyPasswordArray) throws IOException, GeneralSecurityException { + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(new BouncyCastleProvider()); + } + List certificates = new ArrayList<>(); + PrivateKey privateKey = null; + JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter(); + JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter(); + try (InputStream inStream = ResourceUtils.getInputStream(this, this.certFile)) { + try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) { + Object object; + while((object = pemParser.readObject()) != null) { + if (object instanceof X509CertificateHolder) { + X509Certificate x509Cert = certConverter.getCertificate((X509CertificateHolder) object); + certificates.add(x509Cert); + } else if (object instanceof PEMEncryptedKeyPair) { + PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray); + privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); + } else if (object instanceof PEMKeyPair) { + privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); + } else if (object instanceof PrivateKeyInfo) { + privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); + } + } + } + } + if (privateKey == null && !StringUtils.isEmpty(this.keyFile)) { + if (ResourceUtils.resourceExists(this, this.keyFile)) { + try (InputStream inStream = ResourceUtils.getInputStream(this, this.keyFile)) { + try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) { + Object object; + while ((object = pemParser.readObject()) != null) { + if (object instanceof PEMEncryptedKeyPair) { + PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray); + privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); + break; + } else if (object instanceof PEMKeyPair) { + privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); + break; + } else if (object instanceof PrivateKeyInfo) { + privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); + } + } + } + } + } + } + if (certificates.isEmpty()) { + throw new IllegalArgumentException("No certificates found in certFile: " + this.certFile); + } + if (privateKey == null && !trustsOnly) { + throw new IllegalArgumentException("Unable to load private key neither from certFile: " + this.certFile + " nor from keyFile: " + this.keyFile); + } + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + if (trustsOnly) { + List unique = certificates.stream().distinct().collect(Collectors.toList()); + for (int i = 0; i < unique.size(); i++) { + keyStore.setCertificateEntry("root-" + i, unique.get(i)); + } + } + if (privateKey != null) { + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + CertPath certPath = factory.generateCertPath(certificates); + List path = certPath.getCertificates(); + Certificate[] x509Certificates = path.toArray(new Certificate[0]); + keyStore.setKeyEntry(DEFAULT_KEY_ALIAS, privateKey, keyPasswordArray, x509Certificates); + } + return keyStore; + } + + @Override + public String getKeyAlias() { + return DEFAULT_KEY_ALIAS; + } + + @Override + protected void updateKeyAlias(String keyAlias) { + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java new file mode 100644 index 0000000000..c6207b877d --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java @@ -0,0 +1,52 @@ +/** + * 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.transport.config.ssl; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.X509Certificate; + +public interface SslCredentials { + + void init(boolean trustsOnly) throws IOException, GeneralSecurityException; + + KeyStore getKeyStore(); + + String getKeyPassword(); + + String getKeyAlias(); + + PrivateKey getPrivateKey(); + + PublicKey getPublicKey(); + + X509Certificate[] getCertificateChain(); + + X509Certificate[] getTrustedCertificates(); + + TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException; + + KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java new file mode 100644 index 0000000000..8b43f36574 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java @@ -0,0 +1,66 @@ +/** + * 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.transport.config.ssl; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.PostConstruct; + +@Slf4j +@Data +public class SslCredentialsConfig { + + private boolean enabled = true; + private SslCredentialsType type; + private PemSslCredentials pem; + private KeystoreSslCredentials keystore; + + private SslCredentials credentials; + + private final String name; + private final boolean trustsOnly; + + public SslCredentialsConfig(String name, boolean trustsOnly) { + this.name = name; + this.trustsOnly = trustsOnly; + } + + @PostConstruct + public void init() { + if (this.enabled) { + log.info("{}: Initializing SSL credentials.", name); + if (SslCredentialsType.PEM.equals(type) && pem.canUse()) { + this.credentials = this.pem; + } else if (keystore.canUse()) { + if (SslCredentialsType.PEM.equals(type)) { + log.warn("{}: Specified PEM configuration is not valid. Using SSL keystore configuration as fallback.", name); + } + this.credentials = this.keystore; + } else { + throw new RuntimeException(name + ": Invalid SSL credentials configuration. None of the PEM or KEYSTORE configurations can be used!"); + } + try { + this.credentials.init(this.trustsOnly); + } catch (Exception e) { + throw new RuntimeException(name + ": Failed to init SSL credentials configuration.", e); + } + } else { + log.info("{}: Skipping initialization of disabled SSL credentials.", name); + } + } + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsType.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsType.java new file mode 100644 index 0000000000..64115e57b6 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsType.java @@ -0,0 +1,21 @@ +/** + * 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.transport.config.ssl; + +public enum SslCredentialsType { + PEM, + KEYSTORE +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java new file mode 100644 index 0000000000..054d4f6195 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsWebServerCustomizer.java @@ -0,0 +1,71 @@ +/** + * 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.transport.config.ssl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.SslStoreProvider; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import java.security.KeyStore; + +@Component +@ConditionalOnExpression("'${spring.main.web-environment:true}'=='true' && '${server.ssl.enabled:false}'=='true'") +public class SslCredentialsWebServerCustomizer implements WebServerFactoryCustomizer { + + @Bean + @ConfigurationProperties(prefix = "server.ssl.credentials") + public SslCredentialsConfig httpServerSslCredentials() { + return new SslCredentialsConfig("HTTP Server SSL Credentials", false); + } + + @Autowired + @Qualifier("httpServerSslCredentials") + private SslCredentialsConfig httpServerSslCredentialsConfig; + + private final ServerProperties serverProperties; + + public SslCredentialsWebServerCustomizer(ServerProperties serverProperties) { + this.serverProperties = serverProperties; + } + + @Override + public void customize(ConfigurableServletWebServerFactory factory) { + SslCredentials sslCredentials = this.httpServerSslCredentialsConfig.getCredentials(); + Ssl ssl = serverProperties.getSsl(); + ssl.setKeyAlias(sslCredentials.getKeyAlias()); + ssl.setKeyPassword(sslCredentials.getKeyPassword()); + factory.setSsl(ssl); + factory.setSslStoreProvider(new SslStoreProvider() { + @Override + public KeyStore getKeyStore() { + return sslCredentials.getKeyStore(); + } + + @Override + public KeyStore getTrustStore() { + return null; + } + }); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java index e7d6cddc3c..5ee4064466 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -57,7 +57,7 @@ public class DefaultTransportRateLimitService implements TransportRateLimitServi @Override public EntityType checkLimits(TenantId tenantId, DeviceId deviceId, int dataPoints) { if (!tenantAllowed.getOrDefault(tenantId, Boolean.TRUE)) { - return EntityType.TENANT; + return EntityType.API_USAGE_STATE; } if (!checkEntityRateLimit(dataPoints, getTenantRateLimits(tenantId))) { return EntityType.TENANT; 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 d83aa4d4d9..52e52c30f1 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 @@ -1184,7 +1184,7 @@ public class DefaultTransportService implements TransportService { @Scheduled(fixedDelayString = "${transport.stats.print-interval-ms:60000}") public void printStats() { - if (statsEnabled) { + if (statsEnabled && !statsMap.isEmpty()) { String values = statsMap.entrySet().stream() .map(kv -> kv.getKey() + " [" + kv.getValue() + "]").collect(Collectors.joining(", ")); log.info("Transport Stats: {}", values); diff --git a/common/util/pom.xml b/common/util/pom.xml index a49b602c19..2109d090b1 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT common org.thingsboard.common 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 9384417a76..573a90b50f 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 3b10f624d7..d7fe41e8c7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.Dao; import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -59,4 +61,9 @@ public interface AlarmDao extends Dao { PageData findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); + void createEntityAlarmRecord(EntityAlarm entityAlarm); + + List findEntityAlarmRecords(TenantId tenantId, AlarmId id); + + void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 90d988c20b..eb6d5c778e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; @@ -45,7 +46,6 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityService; @@ -166,23 +166,24 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ private AlarmOperationResult createAlarm(Alarm alarm) throws InterruptedException, ExecutionException { log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); - List propagatedEntitiesList = createAlarmRelations(saved); + List propagatedEntitiesList = createEntityAlarmRecords(saved); return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); } - private List createAlarmRelations(Alarm alarm) throws InterruptedException, ExecutionException { + private List createEntityAlarmRecords(Alarm alarm) throws InterruptedException, ExecutionException { List propagatedEntitiesList; if (alarm.isPropagate()) { Set parentEntities = getParentEntities(alarm); propagatedEntitiesList = new ArrayList<>(parentEntities.size() + 1); for (EntityId parentId : parentEntities) { propagatedEntitiesList.add(parentId); - createAlarmRelation(alarm.getTenantId(), parentId, alarm.getId()); + createEntityAlarmRecord(alarm.getTenantId(), parentId, alarm); } propagatedEntitiesList.add(alarm.getOriginator()); } else { propagatedEntitiesList = Collections.singletonList(alarm.getOriginator()); } + createEntityAlarmRecord(alarm.getTenantId(), alarm.getOriginator(), alarm); return propagatedEntitiesList; } @@ -221,7 +222,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ List propagatedEntitiesList; if (!oldPropagate && newPropagate) { try { - propagatedEntitiesList = createAlarmRelations(result); + propagatedEntitiesList = createEntityAlarmRecords(result); } catch (InterruptedException | ExecutionException e) { log.warn("Failed to update alarm relations [{}]", result, e); throw new RuntimeException(e); @@ -348,6 +349,11 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } + @Override + public void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId) { + alarmDao.deleteEntityAlarmRecords(tenantId, entityId); + } + private Alarm merge(Alarm existing, Alarm alarm) { if (alarm.getStartTs() > existing.getEndTs()) { existing.setEndTs(alarm.getStartTs()); @@ -382,17 +388,20 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ private Set getPropagationEntityIds(Alarm alarm) { if (alarm.isPropagate()) { - List relations = relationService.findByTo(alarm.getTenantId(), alarm.getId(), RelationTypeGroup.ALARM); - Set propagationEntityIds = relations.stream().map(EntityRelation::getFrom).collect(Collectors.toSet()); - propagationEntityIds.add(alarm.getOriginator()); - return propagationEntityIds; + List entityAlarms = alarmDao.findEntityAlarmRecords(alarm.getTenantId(), alarm.getId()); + return entityAlarms.stream().map(EntityAlarm::getEntityId).collect(Collectors.toSet()); } else { return Collections.singleton(alarm.getOriginator()); } } - private void createAlarmRelation(TenantId tenantId, EntityId entityId, EntityId alarmId) { - createRelation(tenantId, new EntityRelation(entityId, alarmId, AlarmSearchStatus.ANY.name(), RelationTypeGroup.ALARM)); + private void createEntityAlarmRecord(TenantId tenantId, EntityId entityId, Alarm alarm) { + EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), alarm.getId()); + try { + alarmDao.createEntityAlarmRecord(entityAlarm); + } catch (Exception e) { + log.warn("[{}] Failed to create entity alarm record: {}", tenantId, entityAlarm, e); + } } private ListenableFuture getAndUpdate(TenantId tenantId, AlarmId alarmId, Function function) { @@ -402,7 +411,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } private DataValidator alarmDataValidator = - new DataValidator() { + new DataValidator<>() { @Override protected void validateDataImpl(TenantId tenantId, Alarm alarm) { 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 47af5df8fe..14539066ff 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 @@ -96,7 +96,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 720d50ad69..105352c8a5 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 @@ -167,7 +167,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/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 4d2a299576..970712fc19 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -18,12 +18,14 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; @@ -41,6 +43,10 @@ public abstract class AbstractEntityService { @Autowired protected RelationService relationService; + @Lazy + @Autowired + protected AlarmService alarmService; + @Autowired protected EntityViewService entityViewService; @@ -60,6 +66,8 @@ public abstract class AbstractEntityService { protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) { log.trace("Executing deleteEntityRelations [{}]", entityId); relationService.deleteEntityRelations(tenantId, entityId); + log.trace("Executing deleteEntityAlarms [{}]", entityId); + alarmService.deleteEntityAlarmRelations(tenantId, entityId); } protected Optional extractConstraintViolationException(Exception t) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 7686d9ec21..06e19a7730 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -104,11 +104,6 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Autowired private OtaPackageService otaPackageService; - @Override - public void deleteEntityRelations(TenantId tenantId, EntityId entityId) { - super.deleteEntityRelations(tenantId, entityId); - } - @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index 304b04a33d..33ea3d635f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.EntityViewId; 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.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index e8e90f2d3c..6f7a3c2567 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -257,6 +257,7 @@ public class ModelConstants { /** * Cassandra alarm constants. */ + public static final String ENTITY_ALARM_COLUMN_FAMILY_NAME = "entity_alarm"; public static final String ALARM_COLUMN_FAMILY_NAME = "alarm"; public static final String ALARM_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String ALARM_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; 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/model/sql/EntityAlarmCompositeKey.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmCompositeKey.java new file mode 100644 index 0000000000..69152418b8 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmCompositeKey.java @@ -0,0 +1,42 @@ +/** + * 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.model.sql; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.alarm.EntityAlarm; + +import javax.persistence.Transient; +import java.io.Serializable; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class EntityAlarmCompositeKey implements Serializable { + + @Transient + private static final long serialVersionUID = -245388185894468450L; + + private UUID entityId; + private UUID alarmId; + + public EntityAlarmCompositeKey(EntityAlarm entityAlarm) { + this.entityId = entityAlarm.getEntityId().getId(); + this.alarmId = entityAlarm.getAlarmId().getId(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java new file mode 100644 index 0000000000..50d57cf61c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java @@ -0,0 +1,99 @@ +/** + * 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.model.sql; + +import lombok.Data; +import org.thingsboard.server.common.data.alarm.EntityAlarm; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.ToData; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.Table; +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.CREATED_TIME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ALARM_COLUMN_FAMILY_NAME; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ID_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_TYPE_COLUMN; +import static org.thingsboard.server.dao.model.ModelConstants.TENANT_ID_COLUMN; + +@Data +@Entity +@Table(name = ENTITY_ALARM_COLUMN_FAMILY_NAME) +@IdClass(EntityAlarmCompositeKey.class) +public final class EntityAlarmEntity implements ToData { + + @Column(name = TENANT_ID_COLUMN, columnDefinition = "uuid") + private UUID tenantId; + + @Column(name = ENTITY_TYPE_COLUMN) + private String entityType; + + @Id + @Column(name = ENTITY_ID_COLUMN, columnDefinition = "uuid") + private UUID entityId; + + @Id + @Column(name = "alarm_id", columnDefinition = "uuid") + private UUID alarmId; + + @Column(name = CREATED_TIME_PROPERTY) + private long createdTime; + + @Column(name = "alarm_type") + private String alarmType; + + @Column(name = CUSTOMER_ID_PROPERTY, columnDefinition = "uuid") + private UUID customerId; + + public EntityAlarmEntity() { + super(); + } + + public EntityAlarmEntity(EntityAlarm entityAlarm) { + tenantId = entityAlarm.getTenantId().getId(); + entityId = entityAlarm.getEntityId().getId(); + entityType = entityAlarm.getEntityId().getEntityType().name(); + alarmId = entityAlarm.getAlarmId().getId(); + alarmType = entityAlarm.getAlarmType(); + createdTime = entityAlarm.getCreatedTime(); + if (entityAlarm.getCustomerId() != null) { + customerId = entityAlarm.getCustomerId().getId(); + } + } + + @Override + public EntityAlarm toData() { + EntityAlarm result = new EntityAlarm(); + result.setTenantId(new TenantId(tenantId)); + result.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); + result.setAlarmId(new AlarmId(alarmId)); + result.setAlarmType(alarmType); + result.setCreatedTime(createdTime); + if (customerId != null) { + result.setCustomerId(new CustomerId(customerId)); + } + return result; + } + +} \ No newline at end of file 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 b577022d57..996c0801ff 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 @@ -30,8 +30,11 @@ 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.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; @@ -39,8 +42,10 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import javax.annotation.Nullable; import java.util.ArrayList; @@ -53,6 +58,7 @@ import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE; +import static org.thingsboard.server.dao.service.Validator.validateId; /** * Created by ashvayka on 28.04.17. @@ -193,11 +199,11 @@ public class BaseRelationService implements RelationService { outboundRelations.addAll(relationDao.findAllByFrom(tenantId, entityId, typeGroup)); } - for (EntityRelation relation : inboundRelations){ + for (EntityRelation relation : inboundRelations) { delete(tenantId, cache, relation, true); } - for (EntityRelation relation : outboundRelations){ + for (EntityRelation relation : outboundRelations) { delete(tenantId, cache, relation, false); } @@ -555,10 +561,18 @@ public class BaseRelationService implements RelationService { } } + @Override + public List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit) { + log.trace("Executing findRuleNodeToRuleChainRelations, tenantId [{}], ruleChainType {} and limit {}", tenantId, ruleChainType, limit); + validateId(tenantId, "Invalid tenant id: " + tenantId); + return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit); + } + protected void validate(EntityRelation relation) { if (relation == null) { throw new DataValidationException("Relation type should be specified!"); } + ConstraintValidator.validateFields(relation); validate(relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index d60f176832..85d333b04a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -16,13 +16,11 @@ package org.thingsboard.server.dao.relation; import com.google.common.util.concurrent.ListenableFuture; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChainType; import java.util.List; @@ -63,4 +61,5 @@ public interface RelationDao { ListenableFuture deleteOutboundRelationsAsync(TenantId tenantId, EntityId entity); + List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 2ead3f4334..6b990a7c6c 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; @@ -48,10 +48,13 @@ import org.thingsboard.server.common.data.rule.RuleChainData; import org.thingsboard.server.common.data.rule.RuleChainImportResult; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleChainUpdateResult; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; @@ -59,6 +62,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; @@ -70,6 +74,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.TENANT; import static org.thingsboard.server.dao.service.Validator.validateId; +import static org.thingsboard.server.dao.service.Validator.validateString; /** * Created by igor on 3/12/18. @@ -128,12 +133,14 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional - public boolean saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { + public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); if (ruleChain == null) { - return false; + return RuleChainUpdateResult.failed(); } + ConstraintValidator.validateFields(ruleChainMetaData); + List updatedRuleNodes = new ArrayList<>(); if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { validateCircles(ruleChainMetaData.getConnections()); @@ -158,11 +165,15 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (RuleNode existingNode : existingRuleNodes) { deleteEntityRelations(tenantId, existingNode.getId()); Integer index = ruleNodeIndexMap.get(existingNode.getId()); + RuleNode newRuleNode = null; if (index != null) { - toAddOrUpdate.add(ruleChainMetaData.getNodes().get(index)); + newRuleNode = ruleChainMetaData.getNodes().get(index); + toAddOrUpdate.add(newRuleNode); } else { + updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, null)); toDelete.add(existingNode); } + updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, newRuleNode)); } if (nodes != null) { for (RuleNode node : toAddOrUpdate) { @@ -206,7 +217,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } - return true; + return RuleChainUpdateResult.successful(updatedRuleNodes); } private void validateCircles(List connectionInfos) { @@ -416,41 +427,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 +491,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 +562,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) { @@ -639,6 +660,20 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return ruleChainDao.findAutoAssignToEdgeRuleChainsByTenantId(tenantId.getId(), pageLink); } + @Override + public List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search) { + log.trace("Executing findRuleNodes, tenantId [{}], type {}, search {}", tenantId, type, search); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateString(type, "Incorrect type of the rule node"); + validateString(search, "Incorrect search text"); + return ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId, type, search); + } + + @Override + public RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode) { + return ruleNodeDao.save(tenantId, ruleNode); + } + private void checkRuleNodesAndDelete(TenantId tenantId, RuleChainId ruleChainId) { try { ruleChainDao.removeById(tenantId, ruleChainId.getId()); @@ -723,4 +758,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..bf036a8105 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,13 +15,17 @@ */ 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; +import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; 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.List; import java.util.UUID; /** @@ -74,4 +78,8 @@ 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); + + List getOutputLabelUsage(UUID id, UUID id1); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java index 46bfefc15e..4002867ba9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java @@ -15,12 +15,16 @@ */ package org.thingsboard.server.dao.rule; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.Dao; +import java.util.List; + /** * Created by igor on 3/12/18. */ public interface RuleNodeDao extends Dao { + List findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java new file mode 100644 index 0000000000..30ea042b0a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -0,0 +1,63 @@ +/** + * 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 lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; +import org.hibernate.validator.cfg.ConstraintMapping; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.ValidationException; +import javax.validation.Validator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +public class ConstraintValidator { + + private static Validator fieldsValidator; + + static { + initializeValidators(); + } + + public static void validateFields(Object data) { + Set> constraintsViolations = fieldsValidator.validate(data); + List validationErrors = constraintsViolations.stream() + .map(ConstraintViolation::getMessage) + .distinct() + .collect(Collectors.toList()); + if (!validationErrors.isEmpty()) { + throw new ValidationException("Validation error: " + String.join(", ", validationErrors)); + } + } + + private static void initializeValidators() { + HibernateValidatorConfiguration validatorConfiguration = Validation.byProvider(HibernateValidator.class).configure(); + + ConstraintMapping constraintMapping = validatorConfiguration.createConstraintMapping(); + constraintMapping.constraintDefinition(NoXss.class).validatedBy(NoXssValidator.class); + constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class); + validatorConfiguration.addMapping(constraintMapping); + + fieldsValidator = validatorConfiguration.buildValidatorFactory().getValidator(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index e630624ed4..a0a7a3be2f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -17,51 +17,32 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; -import org.hibernate.validator.HibernateValidator; -import org.hibernate.validator.HibernateValidatorConfiguration; -import org.hibernate.validator.cfg.ConstraintMapping; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.exception.DataValidationException; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; import java.util.HashSet; import java.util.Iterator; -import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; @Slf4j public abstract class DataValidator> { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$", Pattern.CASE_INSENSITIVE); - private static Validator fieldsValidator; - - static { - initializeFieldsValidator(); - } - public void validate(D data, Function tenantIdFunction) { try { if (data == null) { throw new DataValidationException("Data object can't be null!"); } - List validationErrors = validateFields(data); - if (!validationErrors.isEmpty()) { - throw new IllegalArgumentException("Validation error: " + String.join(", ", validationErrors)); - } + ConstraintValidator.validateFields(data); TenantId tenantId = tenantIdFunction.apply(data); validateDataImpl(tenantId, data); @@ -104,14 +85,6 @@ public abstract class DataValidator> { return emailMatcher.matches(); } - private List validateFields(D data) { - Set> constraintsViolations = fieldsValidator.validate(data); - return constraintsViolations.stream() - .map(ConstraintViolation::getMessage) - .distinct() - .collect(Collectors.toList()); - } - protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, TenantEntityDao tenantEntityDao, long maxEntities, @@ -126,10 +99,10 @@ public abstract class DataValidator> { } protected void validateMaxSumDataSizePerTenant(TenantId tenantId, - TenantEntityWithDataDao dataDao, - long maxSumDataSize, - long currentDataSize, - EntityType entityType) { + TenantEntityWithDataDao dataDao, + long maxSumDataSize, + long currentDataSize, + EntityType entityType) { if (maxSumDataSize > 0) { if (dataDao.sumDataSizeByTenantId(tenantId) + currentDataSize > maxSumDataSize) { throw new DataValidationException(String.format("Failed to create the %s, files size limit is exhausted %d bytes!", @@ -156,12 +129,4 @@ public abstract class DataValidator> { } } - private static void initializeFieldsValidator() { - HibernateValidatorConfiguration validatorConfiguration = Validation.byProvider(HibernateValidator.class).configure(); - ConstraintMapping constraintMapping = validatorConfiguration.createConstraintMapping(); - constraintMapping.constraintDefinition(NoXss.class).validatedBy(NoXssValidator.class); - validatorConfiguration.addMapping(constraintMapping); - - fieldsValidator = validatorConfiguration.buildValidatorFactory().getValidator(); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java new file mode 100644 index 0000000000..b7d9d3991e --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/StringLengthValidator.java @@ -0,0 +1,41 @@ +/** + * 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 lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.validation.Length; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +@Slf4j +public class StringLengthValidator implements ConstraintValidator { + private int max; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (StringUtils.isEmpty(value)) { + return true; + } + return value.length() <= max; + } + + @Override + public void initialize(Length constraintAnnotation) { + this.max = constraintAnnotation.max(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 8f862390c3..0152ef058d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -42,51 +42,32 @@ public interface AlarmRepository extends CrudRepository { Pageable pageable); @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + - "LEFT JOIN RelationEntity re ON a.id = re.toId " + - "AND re.relationTypeGroup = 'ALARM' " + - "AND re.toType = 'ALARM' " + - "AND re.fromId = :affectedEntityId " + - "AND re.fromType = :affectedEntityType " + + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + - "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + - "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + - "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ea.tenantId = :tenantId " + + "AND ea.entityId = :affectedEntityId " + + "AND ea.entityType = :affectedEntityType " + + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " , countQuery = "" + - "SELECT count(a) + " + //alarms with relations only - " (SELECT count(a) FROM AlarmEntity a " + //alarms WITHOUT any relations - " LEFT JOIN RelationEntity re ON a.id = re.toId " + - " AND re.relationTypeGroup = 'ALARM' " + - " AND re.toType = 'ALARM' " + - " AND re.fromId = :affectedEntityId " + - " AND re.fromType = :affectedEntityType " + - " WHERE a.tenantId = :tenantId " + - " AND (a.originatorId = :affectedEntityId) " + - " AND (re.fromId IS NULL) " + //anti join - " AND (:startTime IS NULL OR a.createdTime >= :startTime) " + - " AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - " AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - " AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + - " )" + + "SELECT count(a) " + //alarms with relations only "FROM AlarmEntity a " + - "INNER JOIN RelationEntity re ON a.id = re.toId " + - "AND re.relationTypeGroup = 'ALARM' " + - "AND re.toType = 'ALARM' " + - "AND re.fromId = :affectedEntityId " + - "AND re.fromType = :affectedEntityType " + + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + - "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + - "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + + "AND ea.tenantId = :tenantId " + + "AND ea.entityId = :affectedEntityId " + + "AND ea.entityType = :affectedEntityType " + + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") Page findAlarms(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, @@ -101,9 +82,9 @@ public interface AlarmRepository extends CrudRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ", + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ", countQuery = "" + "SELECT count(a) " + "FROM AlarmEntity a " + @@ -111,9 +92,9 @@ public interface AlarmRepository extends CrudRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") Page findAllAlarms(@Param("tenantId") UUID tenantId, @Param("startTime") Long startTime, @Param("endTime") Long endTime, @@ -126,9 +107,9 @@ public interface AlarmRepository extends CrudRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " , countQuery = "" + "SELECT count(a) " + @@ -137,9 +118,9 @@ public interface AlarmRepository extends CrudRepository { "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + - "AND (LOWER(a.type) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.severity) LIKE LOWER(CONCAT(:searchText, '%')) " + - " OR LOWER(a.status) LIKE LOWER(CONCAT(:searchText, '%'))) ") + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") Page findCustomerAlarms(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("startTime") Long startTime, @@ -149,13 +130,11 @@ public interface AlarmRepository extends CrudRepository { Pageable pageable); @Query(value = "SELECT a.severity FROM AlarmEntity a " + - "LEFT JOIN RelationEntity re ON a.id = re.toId " + - "AND re.relationTypeGroup = 'ALARM' " + - "AND re.toType = 'ALARM' " + - "AND re.fromId = :affectedEntityId " + - "AND re.fromType = :affectedEntityType " + + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + - "AND (a.originatorId = :affectedEntityId or re.fromId IS NOT NULL) " + + "AND ea.tenantId = :tenantId " + + "AND ea.entityId = :affectedEntityId " + + "AND ea.entityType = :affectedEntityType " + "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))") Set findAlarmSeverities(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java new file mode 100644 index 0000000000..852b0d2ce6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.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.dao.sql.alarm; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.dao.model.sql.EntityAlarmCompositeKey; +import org.thingsboard.server.dao.model.sql.EntityAlarmEntity; + +import java.util.List; +import java.util.UUID; + +public interface EntityAlarmRepository extends CrudRepository { + + List findAllByAlarmId(UUID alarmId); + + @Transactional + void deleteByEntityId(UUID id); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index 3216a23eee..eaed0480c4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -37,6 +38,7 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.model.sql.AlarmEntity; +import org.thingsboard.server.dao.model.sql.EntityAlarmEntity; import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.query.AlarmQueryRepository; @@ -62,7 +64,7 @@ public class JpaAlarmDao extends JpaAbstractDao implements A private AlarmQueryRepository alarmQueryRepository; @Autowired - private RelationDao relationDao; + private EntityAlarmRepository entityAlarmRepository; @Override protected Class getEntityClass() { @@ -169,4 +171,22 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return DaoUtil.pageToPageData(alarmRepository.findAlarmsIdsByEndTsBeforeAndTenantId(time, tenantId.getId(), DaoUtil.toPageable(pageLink))) .mapData(AlarmId::new); } + + @Override + public void createEntityAlarmRecord(EntityAlarm entityAlarm) { + log.debug("Saving entity {}", entityAlarm); + entityAlarmRepository.save(new EntityAlarmEntity(entityAlarm)); + } + + @Override + public List findEntityAlarmRecords(TenantId tenantId, AlarmId id) { + log.trace("[{}] Try to find entity alarm records using [{}]", tenantId, id); + return DaoUtil.convertDataList(entityAlarmRepository.findAllByAlarmId(id.getId())); + } + + @Override + public void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId) { + log.trace("[{}] Try to delete entity alarm records using [{}]", tenantId, entityId); + entityAlarmRepository.deleteByEntityId(entityId.getId()); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java index 89d1006018..8a61447a00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/AssetRepository.java @@ -39,7 +39,7 @@ public interface AssetRepository extends PagingAndSortingRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -48,14 +48,14 @@ public interface AssetRepository extends PagingAndSortingRepository findAssetInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT a FROM AssetEntity a WHERE a.tenantId = :tenantId " + "AND a.customerId = :customerId " + - "AND LOWER(a.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(a.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -66,7 +66,7 @@ public interface AssetRepository extends PagingAndSortingRepository findAssetInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -80,7 +80,7 @@ public interface AssetRepository extends PagingAndSortingRepository findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -91,7 +91,7 @@ public interface AssetRepository extends PagingAndSortingRepository findAssetInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -100,7 +100,7 @@ public interface AssetRepository extends PagingAndSortingRepository findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -113,7 +113,7 @@ public interface AssetRepository extends PagingAndSortingRepository findAssetInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -126,7 +126,7 @@ public interface AssetRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -136,7 +136,7 @@ public interface AssetRepository extends PagingAndSortingRepository findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java index a574cb1581..b0291749d6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -34,11 +34,11 @@ public interface AuditLogRepository extends PagingAndSortingRepository= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + - "AND (LOWER(a.entityType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.userName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + "AND (LOWER(a.entityType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.entityName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.userName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT('%', :textSearch, '%')))" ) Page findByTenantId( @Param("tenantId") UUID tenantId, @@ -54,10 +54,10 @@ public interface AuditLogRepository extends PagingAndSortingRepository= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + - "AND (LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.userName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + "AND (LOWER(a.entityName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.userName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT('%', :textSearch, '%')))" ) Page findAuditLogsByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, @Param("entityType") EntityType entityType, @@ -74,11 +74,11 @@ public interface AuditLogRepository extends PagingAndSortingRepository= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + - "AND (LOWER(a.entityType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.userName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + "AND (LOWER(a.entityType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.entityName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.userName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT('%', :textSearch, '%')))" ) Page findAuditLogsByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @@ -94,10 +94,10 @@ public interface AuditLogRepository extends PagingAndSortingRepository= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + - "AND (LOWER(a.entityType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.entityName) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionType) LIKE LOWER(CONCAT(:textSearch, '%'))" + - "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT(:textSearch, '%')))" + "AND (LOWER(a.entityType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.entityName) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" + + "OR LOWER(a.actionStatus) LIKE LOWER(CONCAT('%', :textSearch, '%')))" ) Page findAuditLogsByTenantIdAndUserId(@Param("tenantId") UUID tenantId, @Param("userId") UUID userId, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java index 115a78068d..5f2621ed11 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/component/ComponentDescriptorRepository.java @@ -34,13 +34,13 @@ public interface ComponentDescriptorRepository extends PagingAndSortingRepositor ComponentDescriptorEntity findByClazz(String clazz); @Query("SELECT cd FROM ComponentDescriptorEntity cd WHERE cd.type = :type " + - "AND LOWER(cd.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(cd.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByType(@Param("type") ComponentType type, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT cd FROM ComponentDescriptorEntity cd WHERE cd.type = :type " + - "AND cd.scope = :scope AND LOWER(cd.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND cd.scope = :scope AND LOWER(cd.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByScopeAndType(@Param("type") ComponentType type, @Param("scope") ComponentScope scope, @Param("textSearch") String textSearch, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java index d91563917f..fb8005250b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/customer/CustomerRepository.java @@ -30,7 +30,7 @@ import java.util.UUID; public interface CustomerRepository extends PagingAndSortingRepository { @Query("SELECT c FROM CustomerEntity c WHERE c.tenantId = :tenantId " + - "AND LOWER(c.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(c.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index b913164d03..68b33c59fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -32,14 +32,14 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT di FROM DashboardInfoEntity di WHERE di.tenantId = :tenantId " + "AND di.mobileHide = false " + - "AND LOWER(di.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(di.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findMobileByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @@ -47,7 +47,7 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -57,7 +57,7 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository findMobileByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -66,7 +66,7 @@ public interface DashboardInfoRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, 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..5e2155f34f 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 " + @@ -34,21 +35,21 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository findDeviceProfiles(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d WHERE " + - "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "d.tenantId = :tenantId AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.image, d.defaultDashboardId, d.type, d.transportType) " + "FROM DeviceProfileEntity d WHERE " + - "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceProfileInfos(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, @Param("transportType") DeviceTransportType transportType, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java index 8296dc9bf3..2deae0bbe1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceRepository.java @@ -42,7 +42,7 @@ public interface DeviceRepository extends JpaRepository { @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -50,7 +50,7 @@ public interface DeviceRepository extends JpaRepository { @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :profileId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndProfileId(@Param("tenantId") UUID tenantId, @Param("profileId") UUID profileId, @Param("searchText") String searchText, @@ -62,7 +62,7 @@ public interface DeviceRepository extends JpaRepository { "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + "WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findDeviceInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -73,7 +73,7 @@ public interface DeviceRepository extends JpaRepository { Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -83,14 +83,14 @@ public interface DeviceRepository extends JpaRepository { "LEFT JOIN CustomerEntity c on c.id = d.customerId " + "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + "WHERE d.tenantId = :tenantId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -99,7 +99,7 @@ public interface DeviceRepository extends JpaRepository { @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + "AND d.firmwareId = null " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndTypeAndFirmwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("textSearch") String textSearch, @@ -108,7 +108,7 @@ public interface DeviceRepository extends JpaRepository { @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + "AND d.softwareId = null " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndTypeAndSoftwareIdIsNull(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("textSearch") String textSearch, @@ -132,7 +132,7 @@ public interface DeviceRepository extends JpaRepository { "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + "WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -144,7 +144,7 @@ public interface DeviceRepository extends JpaRepository { "LEFT JOIN DeviceProfileEntity p on p.id = d.deviceProfileId " + "WHERE d.tenantId = :tenantId " + "AND d.deviceProfileId = :deviceProfileId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceInfosByTenantIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("textSearch") String textSearch, @@ -153,7 +153,7 @@ public interface DeviceRepository extends JpaRepository { @Query("SELECT d FROM DeviceEntity d WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -167,7 +167,7 @@ public interface DeviceRepository extends JpaRepository { "WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -181,7 +181,7 @@ public interface DeviceRepository extends JpaRepository { "WHERE d.tenantId = :tenantId " + "AND d.customerId = :customerId " + "AND d.deviceProfileId = :deviceProfileId " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("deviceProfileId") UUID deviceProfileId, @@ -204,7 +204,7 @@ public interface DeviceRepository extends JpaRepository { @Query("SELECT d FROM DeviceEntity d, RelationEntity re WHERE d.tenantId = :tenantId " + "AND d.id = re.toId AND re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -214,7 +214,7 @@ public interface DeviceRepository extends JpaRepository { "AND d.id = re.toId AND re.toType = 'DEVICE' AND re.relationTypeGroup = 'EDGE' " + "AND re.relationType = 'Contains' AND re.fromId = :edgeId AND re.fromType = 'EDGE' " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, 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..48aaffb96e 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/EdgeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java index f12dda790d..cd0fcd8933 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/EdgeRepository.java @@ -31,7 +31,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("textSearch") String textSearch, @@ -44,7 +44,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -53,14 +53,14 @@ public interface EdgeRepository extends PagingAndSortingRepository findEdgeInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT d FROM EdgeEntity d WHERE d.tenantId = :tenantId " + "AND d.type = :type " + - "AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(d.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -71,7 +71,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findEdgeInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -80,7 +80,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -92,7 +92,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findEdgeInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -104,7 +104,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findEdgeInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -114,7 +114,7 @@ public interface EdgeRepository extends PagingAndSortingRepository findByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, @Param("entityId") UUID entityId, @Param("entityType") String entityType, 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..ff5da30f31 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,10 +39,12 @@ 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; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -107,6 +109,7 @@ public class JpaBaseEdgeEventDao extends JpaAbstractSearchTextDao findByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @@ -48,14 +48,14 @@ public interface EntityViewRepository extends PagingAndSortingRepository findEntityViewInfosByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT e FROM EntityViewEntity e WHERE e.tenantId = :tenantId " + "AND e.type = :type " + - "AND LOWER(e.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(e.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -66,7 +66,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findEntityViewInfosByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") String type, @Param("textSearch") String textSearch, @@ -74,7 +74,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -85,7 +85,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findEntityViewInfosByTenantIdAndCustomerId(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -94,7 +94,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -107,7 +107,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findEntityViewInfosByTenantIdAndCustomerIdAndType(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("type") String type, @@ -124,7 +124,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -134,7 +134,7 @@ public interface EntityViewRepository extends PagingAndSortingRepository findByTenantIdAndEdgeIdAndType(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("type") String type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java index 270ab26329..84d2a484e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/EventRepository.java @@ -55,7 +55,7 @@ public interface EventRepository extends PagingAndSortingRepository= :startTime) " + "AND (:endTime IS NULL OR e.createdTime <= :endTime) " + - "AND LOWER(e.eventType) LIKE LOWER(CONCAT(:textSearch, '%'))" + "AND LOWER(e.eventType) LIKE LOWER(CONCAT('%', :textSearch, '%'))" ) Page findEventsByTenantIdAndEntityId(@Param("tenantId") UUID tenantId, @Param("entityType") EntityType entityType, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventCleanupRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventCleanupRepository.java index 7ea832ee32..38299e9443 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventCleanupRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/HsqlEventCleanupRepository.java @@ -23,6 +23,7 @@ import org.thingsboard.server.dao.util.HsqlDao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.concurrent.TimeUnit; @Slf4j @HsqlDao @@ -35,6 +36,7 @@ public class HsqlEventCleanupRepository extends JpaAbstractDaoListeningExecutorS try (Connection connection = dataSource.getConnection(); PreparedStatement stmt = connection.prepareStatement("DELETE FROM event WHERE ts < ? AND event_type != 'DEBUG_RULE_NODE' AND event_type != 'DEBUG_RULE_CHAIN'")) { stmt.setLong(1, otherExpirationTime); + stmt.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(1)); stmt.execute(); } catch (SQLException e) { log.error("SQLException occurred during events TTL task execution ", e); @@ -43,6 +45,7 @@ public class HsqlEventCleanupRepository extends JpaAbstractDaoListeningExecutorS try (Connection connection = dataSource.getConnection(); PreparedStatement stmt = connection.prepareStatement("DELETE FROM event WHERE ts < ? AND (event_type = 'DEBUG_RULE_NODE' OR event_type = 'DEBUG_RULE_CHAIN')")) { stmt.setLong(1, debugExpirationTime); + stmt.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(1)); stmt.execute(); } catch (SQLException e) { log.error("SQLException occurred during events TTL task execution ", e); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index c35247ad21..78741a9c1d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -39,10 +39,6 @@ import org.thingsboard.server.dao.event.EventDao; import org.thingsboard.server.dao.model.sql.EventEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -196,7 +192,7 @@ public class JpaBaseEventDao extends JpaAbstractDao 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/event/PsqlEventCleanupRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventCleanupRepository.java index b19c8b0712..7577e0730a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventCleanupRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/PsqlEventCleanupRepository.java @@ -24,6 +24,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.concurrent.TimeUnit; @Slf4j @PsqlDao @@ -37,6 +38,7 @@ public class PsqlEventCleanupRepository extends JpaAbstractDaoListeningExecutorS stmt.setLong(1, otherEventsTtl); stmt.setLong(2, debugEventsTtl); stmt.setLong(3, 0); + stmt.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(1)); stmt.execute(); printWarnings(stmt); try (ResultSet resultSet = stmt.getResultSet()){ diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java index 112b6dd222..3e60afa595 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/ota/OtaPackageInfoRepository.java @@ -28,7 +28,7 @@ import java.util.UUID; public interface OtaPackageInfoRepository extends CrudRepository { @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.tag, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " + "f.tenantId = :tenantId " + - "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(f.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findAllByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @@ -38,7 +38,7 @@ public interface OtaPackageInfoRepository extends CrudRepository findAllByTenantIdAndTypeAndDeviceProfileIdAndHasData(@Param("tenantId") UUID tenantId, @Param("deviceProfileId") UUID deviceProfileId, @Param("type") OtaPackageType type, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 400b12fcab..3fee0f6bef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -105,7 +105,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.propagate_relation_types as propagate_relation_types, " + " a.type as type," + SELECT_ORIGINATOR_NAME + ", "; - private static final String JOIN_RELATIONS = "left join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id and r.from_id in (:entity_ids)"; + private static final String JOIN_ENTITY_ALARMS = "inner join entity_alarm ea on a.id = ea.alarm_id"; protected final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -132,8 +132,8 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { StringBuilder joinPart = new StringBuilder(); boolean addAnd = false; if (pageLink.isSearchPropagatedAlarms()) { - selectPart.append(" CASE WHEN r.from_id IS NULL THEN a.originator_id ELSE r.from_id END as entity_id "); - fromPart.append(JOIN_RELATIONS); + selectPart.append(" ea.entity_id as entity_id "); + fromPart.append(JOIN_ENTITY_ALARMS); wherePart.append(buildPermissionsQuery(tenantId, customerId, ctx)); addAnd = true; } else { @@ -145,7 +145,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey)) .append(" ").append(sortOrder.getDirection().name()); if (pageLink.isSearchPropagatedAlarms()) { - wherePart.append(" and (a.originator_id in (:entity_ids) or r.from_id IS NOT NULL)"); + wherePart.append(" and ea.entity_id in (:entity_ids)"); } else { addAndIfNeeded(wherePart, addAnd); addAnd = true; @@ -166,7 +166,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { } joinPart.append(" as e(id, priority)) e "); if (pageLink.isSearchPropagatedAlarms()) { - joinPart.append("on (r.from_id IS NULL and a.originator_id = e.id) or (r.from_id IS NOT NULL and r.from_id = e.id)"); + joinPart.append("on ea.entity_id = e.id"); } else { joinPart.append("on a.originator_id = e.id"); } @@ -188,6 +188,9 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { addAnd = true; ctx.addLongParameter("startTime", startTs); wherePart.append("a.created_time >= :startTime"); + if (pageLink.isSearchPropagatedAlarms()) { + wherePart.append(" and ea.created_time >= :startTime"); + } } if (endTs > 0) { @@ -195,6 +198,9 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { addAnd = true; ctx.addLongParameter("endTime", endTs); wherePart.append("a.created_time <= :endTime"); + if (pageLink.isSearchPropagatedAlarms()) { + wherePart.append(" and ea.created_time <= :endTime"); + } } if (pageLink.getTypeList() != null && !pageLink.getTypeList().isEmpty()) { @@ -202,6 +208,9 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { addAnd = true; ctx.addStringListParameter("alarmTypes", pageLink.getTypeList()); wherePart.append("a.type in (:alarmTypes)"); + if (pageLink.isSearchPropagatedAlarms()) { + wherePart.append(" and ea.alarm_type in (:alarmTypes)"); + } } if (pageLink.getSeverityList() != null && !pageLink.getSeverityList().isEmpty()) { @@ -279,7 +288,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { private String buildPermissionsQuery(TenantId tenantId, CustomerId customerId, QueryContext ctx) { StringBuilder permissionsQuery = new StringBuilder(); ctx.addUuidParameter("permissions_tenant_id", tenantId.getId()); - permissionsQuery.append(" a.tenant_id = :permissions_tenant_id "); + permissionsQuery.append(" a.tenant_id = :permissions_tenant_id and ea.tenant_id = :permissions_tenant_id "); /* No need to check the customer id, because we already use entity id list that passed security check when we were evaluating the data query. */ 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 f567420947..4b331f78f2 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; @@ -223,7 +225,6 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { private static final String SELECT_API_USAGE_STATE = "(select aus.id, aus.created_time, aus.tenant_id, aus.entity_id, " + "coalesce((select title from tenant where id = aus.entity_id), (select title from customer where id = aus.entity_id)) as name " + "from api_usage_state as aus)"; - static final int MAX_LEVEL_DEFAULT = 50; //This value has to be reasonable small to prevent infinite recursion as early as possible static { entityTableMap.put(EntityType.ASSET, "asset"); @@ -268,6 +269,10 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { 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; @@ -384,6 +389,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } else { entityTypeStr = "'" + entityType.name() + "'"; } + if (!StringUtils.isEmpty(entityFieldsSelection)) { entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection); } else { @@ -437,10 +443,10 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get(); String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc"; if (sortOrderMapping.getEntityKey().getType() == EntityKeyType.ENTITY_FIELD) { - dataQuery = String.format("%s order by %s %s", dataQuery, sortOrderMapping.getValueAlias(), direction); + dataQuery = String.format("%s order by %s %s, result.id %s", dataQuery, sortOrderMapping.getValueAlias(), direction, direction); } else { - dataQuery = String.format("%s order by %s %s, %s %s", dataQuery, - sortOrderMapping.getSortOrderNumAlias(), direction, sortOrderMapping.getSortOrderStrAlias(), direction); + dataQuery = String.format("%s order by %s %s, %s %s, result.id %s", dataQuery, + sortOrderMapping.getSortOrderNumAlias(), direction, sortOrderMapping.getSortOrderStrAlias(), direction, direction); } } } @@ -711,7 +717,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { } int getMaxLevel(int maxLevel) { - return maxLevel > 0 ? maxLevel : MAX_LEVEL_DEFAULT; + 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/query/DefaultQueryLogComponent.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java index 89bf4d1ac1..737c148d49 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultQueryLogComponent.java @@ -33,7 +33,7 @@ public class DefaultQueryLogComponent implements QueryLogComponent { @Override public void logQuery(QueryContext ctx, String query, long duration) { if (logSqlQueries && duration > logQueriesThreshold) { - log.info("QUERY: {} took {}ms", query, duration); + log.info("QUERY: {} took {} ms", query, duration); Arrays.asList(ctx.getParameterNames()).forEach(param -> log.info("QUERY PARAM: {} -> {}", param, ctx.getValue(param))); } } 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 91cc1a0c9f..4b021114f7 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 @@ -19,6 +19,7 @@ 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.domain.PageRequest; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; @@ -26,6 +27,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RelationCompositeKey; import org.thingsboard.server.dao.model.sql.RelationEntity; @@ -157,7 +159,11 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple private boolean deleteRelationIfExists(RelationCompositeKey key) { boolean relationExistsBeforeDelete = relationRepository.existsById(key); if (relationExistsBeforeDelete) { - relationRepository.deleteById(key); + try { + relationRepository.deleteById(key); + } catch (ConcurrencyFailureException e) { + log.debug("[{}] Concurrency exception while deleting relation", key, e); + } } return relationExistsBeforeDelete; } @@ -192,11 +198,16 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple }); } + @Override + public List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit) { + return DaoUtil.convertDataList(relationRepository.findRuleNodeToRuleChainRelations(ruleChainType, PageRequest.of(0, limit))); + } + private Specification getEntityFieldsSpec(EntityId from, String relationType, RelationTypeGroup typeGroup, EntityType childType) { return (root, criteriaQuery, criteriaBuilder) -> { List predicates = new ArrayList<>(); if (from != null) { - Predicate fromIdPredicate = criteriaBuilder.equal(root.get("fromId"), from.getId()); + Predicate fromIdPredicate = criteriaBuilder.equal(root.get("fromId"), from.getId()); predicates.add(fromIdPredicate); Predicate fromEntityTypePredicate = criteriaBuilder.equal(root.get("fromType"), from.getEntityType().name()); predicates.add(fromEntityTypePredicate); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java index c5a1774ae6..65c888f39e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationRepository.java @@ -15,11 +15,20 @@ */ package org.thingsboard.server.dao.sql.relation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.model.sql.RelationCompositeKey; import org.thingsboard.server.dao.model.sql.RelationEntity; +import org.thingsboard.server.dao.model.sql.RuleChainEntity; import java.util.List; import java.util.UUID; @@ -48,6 +57,11 @@ public interface RelationRepository List findAllByFromIdAndFromType(UUID fromId, String fromType); + @Query("SELECT r FROM RelationEntity r WHERE " + + "r.relationTypeGroup = 'RULE_NODE' AND r.toType = 'RULE_CHAIN' " + + "AND r.toId in (SELECT id from RuleChainEntity where type = :ruleChainType )") + List findRuleNodeToRuleChainRelations(@Param("ruleChainType") RuleChainType ruleChainType, Pageable page); + @Transactional S save(S entity); @@ -56,4 +70,5 @@ public interface RelationRepository @Transactional void deleteByFromIdAndFromType(UUID fromId, String fromType); + } 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..b432b04f04 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 @@ -23,12 +23,15 @@ 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; +import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; 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.List; import java.util.Objects; import java.util.UUID; @@ -97,8 +100,19 @@ public class JpaRuleChainDao extends JpaAbstractSearchTextDao findByTenantIdAndTypeAndName(TenantId tenantId, RuleChainType type, String name) { + return DaoUtil.convertDataList(ruleChainRepository.findByTenantIdAndTypeAndName(tenantId.getId(), type, name)); + } + + @Override + public List getOutputLabelUsage(UUID tenantId, UUID ruleChainId) { + return null; + } + @Override public Long countByTenantId(TenantId tenantId) { return ruleChainRepository.countByTenantId(tenantId.getId()); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index 362299f9b3..2e7d796cb3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -19,11 +19,14 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; import org.thingsboard.server.dao.rule.RuleNodeDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; +import java.util.List; import java.util.UUID; @Slf4j @@ -43,4 +46,8 @@ public class JpaRuleNodeDao extends JpaAbstractSearchTextDao findRuleNodesByTenantIdAndType(TenantId tenantId, String type, String search) { + return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByTenantIdAndType(tenantId.getId(), type, search)); + } } 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..9b80dad34f 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 @@ -20,22 +20,27 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.dao.model.sql.RelationEntity; import org.thingsboard.server.dao.model.sql.RuleChainEntity; +import java.util.List; import java.util.UUID; public interface RuleChainRepository extends PagingAndSortingRepository { @Query("SELECT rc FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId " + - "AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(rc.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT rc FROM RuleChainEntity rc WHERE rc.tenantId = :tenantId " + "AND rc.type = :type " + - "AND LOWER(rc.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") + "AND LOWER(rc.searchText) LIKE LOWER(CONCAT('%', :searchText, '%'))") Page findByTenantIdAndType(@Param("tenantId") UUID tenantId, @Param("type") RuleChainType type, @Param("searchText") String searchText, @@ -44,7 +49,7 @@ public interface RuleChainRepository extends PagingAndSortingRepository findByTenantIdAndEdgeId(@Param("tenantId") UUID tenantId, @Param("edgeId") UUID edgeId, @Param("searchText") String searchText, @@ -53,12 +58,16 @@ 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/sql/rule/RuleNodeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java index 09e4bb55ae..77bd5d4a38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java @@ -15,11 +15,21 @@ */ package org.thingsboard.server.dao.sql.rule; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.thingsboard.server.dao.model.sql.RuleNodeEntity; +import java.util.List; import java.util.UUID; public interface RuleNodeRepository extends CrudRepository { + @Query("SELECT r FROM RuleNodeEntity r WHERE r.ruleChainId in " + + "(select id from RuleChainEntity rc WHERE rc.tenantId = :tenantId) " + + "AND r.type = :ruleType AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ") + List findRuleNodesByTenantIdAndType(@Param("tenantId") UUID tenantId, + @Param("ruleType") String ruleType, + @Param("searchText") String searchText); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java index 523d524f6c..a1a6603c9a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantProfileRepository.java @@ -33,13 +33,13 @@ public interface TenantProfileRepository extends PagingAndSortingRepository findTenantProfiles(@Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(t.id, 'TENANT_PROFILE', t.name) " + "FROM TenantProfileEntity t " + - "WHERE LOWER(t.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "WHERE LOWER(t.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findTenantProfileInfos(@Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java index 8ab12e0bb5..4fc4b04885 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/tenant/TenantRepository.java @@ -37,7 +37,7 @@ public interface TenantRepository extends PagingAndSortingRepository findByRegionNextPage(@Param("region") String region, @Param("textSearch") String textSearch, Pageable pageable); @@ -46,7 +46,7 @@ public interface TenantRepository extends PagingAndSortingRepository findTenantInfoByRegionNextPage(@Param("region") String region, @Param("textSearch") String textSearch, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 32357ff921..477b7e8892 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -34,7 +34,7 @@ public interface UserRepository extends PagingAndSortingRepository findUsersByAuthority(@Param("tenantId") UUID tenantId, @Param("customerId") UUID customerId, @Param("searchText") String searchText, @@ -42,7 +42,7 @@ public interface UserRepository extends PagingAndSortingRepository findByTenantId(@Param("tenantId") UUID tenantId, @Param("searchText") String searchText, Pageable pageable); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index 5a3bb8b973..2b6abd5dac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -32,19 +32,19 @@ public interface WidgetsBundleRepository extends PagingAndSortingRepository findSystemWidgetsBundles(@Param("systemTenantId") UUID systemTenantId, @Param("searchText") String searchText, Pageable pageable); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :tenantId " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(wb.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, @Param("textSearch") String textSearch, Pageable pageable); @Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId IN (:tenantId, :nullTenantId) " + - "AND LOWER(wb.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))") + "AND LOWER(wb.searchText) LIKE LOWER(CONCAT('%', :textSearch, '%'))") Page findAllTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, @Param("nullTenantId") UUID nullTenantId, @Param("textSearch") String textSearch, diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java index eb12984753..a5eee9c0f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -74,6 +74,7 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries stmt.setObject(1, ModelConstants.NULL_UUID); stmt.setLong(2, systemTtl); stmt.setLong(3, 0); + stmt.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(1)); stmt.execute(); printWarnings(stmt); try (ResultSet resultSet = stmt.getResultSet()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java index c23e615548..6b9078a3b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/psql/JpaPsqlTimeseriesDao.java @@ -47,6 +47,7 @@ import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @Component @@ -111,6 +112,7 @@ public class JpaPsqlTimeseriesDao extends AbstractChunkedAggregationTimeseriesDa stmt.setString(1, partitioning); stmt.setLong(2, systemTtl); stmt.setLong(3, 0); + stmt.setQueryTimeout((int) TimeUnit.HOURS.toSeconds(1)); stmt.execute(); printWarnings(stmt); try (ResultSet resultSet = stmt.getResultSet()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index eab0a070f5..7314f54b39 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; @@ -55,6 +56,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe private static final String DEFAULT_TENANT_REGION = "Global"; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + @Value("${zk.enabled}") + private Boolean zkEnabled; + @Autowired private TenantDao tenantDao; @@ -190,6 +194,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe if (!StringUtils.isEmpty(tenant.getEmail())) { validateEmail(tenant.getEmail()); } + validateTenantProfile(tenantId, tenant); } @Override @@ -198,6 +203,14 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe if (old == null) { throw new DataValidationException("Can't update non existing tenant!"); } + validateTenantProfile(tenantId, tenant); + } + + private void validateTenantProfile(TenantId tenantId, Tenant tenant) { + TenantProfile tenantProfileById = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId()); + if (!zkEnabled && (tenantProfileById.isIsolatedTbCore() || tenantProfileById.isIsolatedTbRuleEngine())) { + throw new DataValidationException("Can't use isolated tenant profiles in monolith setup!"); + } } }; 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/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index e58ca10459..c5931a8964 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -43,6 +43,18 @@ CREATE TABLE IF NOT EXISTS alarm ( type varchar(255) ); +CREATE TABLE IF NOT EXISTS entity_alarm ( + tenant_id uuid NOT NULL, + entity_type varchar(32), + entity_id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_type varchar(255) NOT NULL, + customer_id uuid, + alarm_id uuid, + CONSTRAINT entity_alarm_pkey PRIMARY KEY(entity_id, alarm_id), + CONSTRAINT fk_entity_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS asset ( id uuid NOT NULL CONSTRAINT asset_pkey PRIMARY KEY, created_time bigint NOT NULL, diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index 78b1eda905..1238b5ee12 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -20,8 +20,12 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_status_created_time ON alarm(tenant_id, status, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_entity_alarm_created_time ON entity_alarm(tenant_id, entity_id, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); CREATE INDEX IF NOT EXISTS idx_relation_from_id ON relation(relation_type_group, from_type, from_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 69760c377a..4fc5fbaebc 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -58,6 +58,18 @@ CREATE TABLE IF NOT EXISTS alarm ( type varchar(255) ); +CREATE TABLE IF NOT EXISTS entity_alarm ( + tenant_id uuid NOT NULL, + entity_type varchar(32), + entity_id uuid NOT NULL, + created_time bigint NOT NULL, + alarm_type varchar(255) NOT NULL, + customer_id uuid, + alarm_id uuid, + CONSTRAINT entity_alarm_pkey PRIMARY KEY (entity_id, alarm_id), + CONSTRAINT fk_entity_alarm_id FOREIGN KEY (alarm_id) REFERENCES alarm(id) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS asset ( id uuid NOT NULL CONSTRAINT asset_pkey PRIMARY KEY, created_time bigint NOT NULL, 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/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index a798133ad4..2d3f21545f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -612,16 +612,6 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, alarms.getData().get(0)); - List toAlarmRelations = relationService.findByTo(tenantId, created.getId(), RelationTypeGroup.ALARM); - Assert.assertEquals(1, toAlarmRelations.size()); - - List fromChildRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM); - Assert.assertEquals(0, fromChildRelations.size()); - - List fromParentRelations = relationService.findByFrom(tenantId, parentId, RelationTypeGroup.ALARM); - Assert.assertEquals(1, fromParentRelations.size()); - - Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()).isSuccessful()); Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); @@ -647,14 +637,5 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - toAlarmRelations = relationService.findByTo(tenantId, created.getId(), RelationTypeGroup.ALARM); - Assert.assertEquals(0, toAlarmRelations.size()); - - fromChildRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM); - Assert.assertEquals(0, fromChildRelations.size()); - - fromParentRelations = relationService.findByFrom(tenantId, childId, RelationTypeGroup.ALARM); - Assert.assertEquals(0, fromParentRelations.size()); - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index 4f110c73e6..ed6b14dd10 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; +import javax.validation.ValidationException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; @@ -673,8 +674,8 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); - thrown.expect(DataValidationException.class); - thrown.expectMessage("The length of title should be equal or shorter than 255"); + thrown.expect(ValidationException.class); + thrown.expectMessage("length of title must be equal or less than 255"); otaPackageService.saveOtaPackageInfo(firmwareInfo, true); } @@ -689,7 +690,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setTitle(TITLE); firmwareInfo.setVersion(RandomStringUtils.random(257)); - thrown.expectMessage("The length of version should be equal or shorter than 255"); + thrown.expectMessage("length of version must be equal or less than 255"); otaPackageService.saveOtaPackageInfo(firmwareInfo, true); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 6f5ea9acb9..27dc7f812a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -31,6 +33,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.exception.DataValidationException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; @@ -84,6 +87,23 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.deleteRelationAsync(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get()); } + @Test + public void testDeleteRelationConcurrently() throws ExecutionException, InterruptedException { + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + + EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + saveRelation(relationA); + + List> futures = new ArrayList<>(); + for (int i = 0; i < 2; i++) { + futures.add(relationService.deleteRelationAsync(SYSTEM_TENANT_ID, relationA)); + } + List results = Futures.allAsList(futures).get(); + Assert.assertTrue(results.contains(true)); + } + @Test public void testDeleteEntityRelations() throws ExecutionException, InterruptedException { AssetId parentId = new AssetId(Uuids.timeBased()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index 109814d330..c1635a6281 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -293,7 +293,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleNodes.set(name3Index, ruleNode4); - Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData)); + Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData).isSuccess()); RuleChainMetaData updatedRuleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, savedRuleChainMetaData.getRuleChainId()); Assert.assertEquals(3, updatedRuleChainMetaData.getNodes().size()); @@ -406,7 +406,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainMetaData.addConnectionInfo(0,2,"fail"); ruleChainMetaData.addConnectionInfo(1,2,"success"); - Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData)); + Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData).isSuccess()); return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java index 5930f7ae4d..29b0558c71 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java @@ -20,8 +20,12 @@ import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -253,4 +257,22 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { Assert.assertTrue(pageData.getData().isEmpty()); } + + @Test(expected = DataValidationException.class) + public void testSaveTenantWithIsolatedProfileInMonolithSetup() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Isolated Tenant Profile"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(new DefaultTenantProfileConfiguration()); + tenantProfile.setProfileData(profileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbCore(true); + tenantProfile.setIsolatedTbRuleEngine(true); + TenantProfile isolatedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + + Tenant tenant = new Tenant(); + tenant.setTitle("Tenant"); + tenant.setTenantProfileId(isolatedTenantProfile.getId()); + tenantService.saveTenant(tenant); + } } 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 index a0c716d51f..1a0ce4cebc 100644 --- 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 @@ -16,42 +16,54 @@ package org.thingsboard.server.dao.sql.query; import org.junit.Test; -import org.thingsboard.server.common.data.id.CustomerId; +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.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.BDDMockito.willCallRealMethod; -import static org.mockito.Mockito.mock; +@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(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT, equalTo(10)); + assertThat(repo.getMaxLevelAllowed(), equalTo(50)); } @Test public void givenMaxLevelZeroOrNegative_whenGetMaxLevel_thenReturnDefaultMaxLevel() { - DefaultEntityQueryRepository repo = mock(DefaultEntityQueryRepository.class); - willCallRealMethod().given(repo).getMaxLevel(anyInt()); - assertThat(repo.getMaxLevel(0), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT)); - assertThat(repo.getMaxLevel(-1), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT)); - assertThat(repo.getMaxLevel(-2), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT)); - assertThat(repo.getMaxLevel(Integer.MIN_VALUE), equalTo(DefaultEntityQueryRepository.MAX_LEVEL_DEFAULT)); + 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() { - DefaultEntityQueryRepository repo = mock(DefaultEntityQueryRepository.class); - willCallRealMethod().given(repo).getMaxLevel(anyInt()); assertThat(repo.getMaxLevel(1), equalTo(1)); assertThat(repo.getMaxLevel(2), equalTo(2)); - assertThat(repo.getMaxLevel(Integer.MAX_VALUE), equalTo(Integer.MAX_VALUE)); + 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/sql/hsql/drop-all-tables.sql b/dao/src/test/resources/sql/hsql/drop-all-tables.sql index 2cd7e26b48..095a38533e 100644 --- a/dao/src/test/resources/sql/hsql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/hsql/drop-all-tables.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS admin_settings; +DROP TABLE IF EXISTS entity_alarm; DROP TABLE IF EXISTS alarm; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; diff --git a/dao/src/test/resources/sql/psql/drop-all-tables.sql b/dao/src/test/resources/sql/psql/drop-all-tables.sql index 0a71953032..3d1d38a0e7 100644 --- a/dao/src/test/resources/sql/psql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/psql/drop-all-tables.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS admin_settings; +DROP TABLE IF EXISTS entity_alarm; DROP TABLE IF EXISTS alarm; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; diff --git a/dao/src/test/resources/sql/timescale/drop-all-tables.sql b/dao/src/test/resources/sql/timescale/drop-all-tables.sql index 53da64aff2..f113ec5277 100644 --- a/dao/src/test/resources/sql/timescale/drop-all-tables.sql +++ b/dao/src/test/resources/sql/timescale/drop-all-tables.sql @@ -1,4 +1,5 @@ DROP TABLE IF EXISTS admin_settings; +DROP TABLE IF EXISTS entity_alarm; DROP TABLE IF EXISTS alarm; DROP TABLE IF EXISTS asset; DROP TABLE IF EXISTS audit_log; diff --git a/docker/haproxy/config/haproxy.cfg b/docker/haproxy/config/haproxy.cfg index 16b8af0595..aa752502bf 100644 --- a/docker/haproxy/config/haproxy.cfg +++ b/docker/haproxy/config/haproxy.cfg @@ -69,14 +69,13 @@ frontend http-in acl transport_http_acl path_beg /api/v1/ acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/ - acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ - acl tb_rulenode_assets path_reg ^/assets/help/.*/rulenode/.*$ + acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ redirect scheme https if !letsencrypt_http_acl !transport_http_acl { env(FORCE_HTTPS_REDIRECT) -m str true } use_backend letsencrypt_http if letsencrypt_http_acl use_backend tb-http-backend if transport_http_acl - use_backend tb-api-backend if tb_api_acl or tb_rulenode_assets + use_backend tb-api-backend if tb_api_acl default_backend tb-web-backend @@ -88,11 +87,10 @@ frontend https_in http-request add-header "X-Forwarded-Proto" "https" acl transport_http_acl path_beg /api/v1/ - acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ - acl tb_rulenode_assets path_reg ^/assets/help/.*/rulenode/.*$ + acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /v3/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/ use_backend tb-http-backend if transport_http_acl - use_backend tb-api-backend if tb_api_acl or tb_rulenode_assets + use_backend tb-api-backend if tb_api_acl default_backend tb-web-backend diff --git a/k8s/.env b/k8s/.env deleted file mode 100644 index 4f0478683b..0000000000 --- a/k8s/.env +++ /dev/null @@ -1,8 +0,0 @@ -# Can be either basic (with single instance of Zookeeper, Kafka and Redis) or high-availability (with Zookeeper, Kafka and Redis in cluster modes). -# According to the deployment type corresponding kubernetes resources will be deployed (see content of the directories ./basic and ./high-availability for details). -DEPLOYMENT_TYPE=basic - -# Database used by ThingsBoard, can be either postgres (PostgreSQL) or hybrid (PostgreSQL for entities database and Cassandra for timeseries database). -# According to the database type corresponding kubernetes resources will be deployed (see postgres.yml, cassandra.yml for details). -DATABASE=postgres - diff --git a/k8s/README.md b/k8s/README.md deleted file mode 100644 index 9d91c8c0e8..0000000000 --- a/k8s/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Kubernetes resources configuration for ThingsBoard Microservices - -This folder containing scripts and Kubernetes resources configurations to run ThingsBoard in Microservices mode. - -## Prerequisites - -ThingsBoard Microservices run on the Kubernetes cluster. -You need to have a Kubernetes cluster, and the kubectl command-line tool must be configured to communicate with your cluster. -If you do not have a cluster already, you can create one by using [Minikube](https://kubernetes.io/docs/setup/minikube), -or you can choose any other available [Kubernetes cluster deployment solutions](https://kubernetes.io/docs/setup/pick-right-solution/). - -### Enable ingress addon - -By default ingress addon is disabled in the Minikube, and available only in cluster providers. -To enable ingress, please execute the following command: - -` -$ minikube addons enable ingress -` - -## Installation - -Before performing initial installation you can configure the type of database to be used with ThingsBoard and the type of deployment. -To set database type change the value of `DATABASE` variable in `.env` file to one of the following: - -- `postgres` - use PostgreSQL database; -- `hybrid` - use PostgreSQL for entities database and Cassandra for timeseries database; - -**NOTE**: According to the database type corresponding kubernetes resources will be deployed (see `postgres.yml`, `cassandra.yml` for details). - -To set deployment type change the value of `DEPLOYMENT_TYPE` variable in `.env` file to one of the following: - -- `basic` - startup with a single instance of Zookeeper, Kafka and Redis; -- `high-availability` - startup with Zookeeper, Kafka, and Redis in cluster modes; - -**NOTE**: According to the deployment type corresponding kubernetes resources will be deployed (see the content of the directories `./basic` and `./high-availability` for details). - -Execute the following command to run the installation: - -` -$ ./k8s-install-tb.sh --loadDemo -` - -Where: - -- `--loadDemo` - optional argument. Whether to load additional demo data. - -## Running - -Execute the following command to deploy third-party resources: - -` -$ ./k8s-deploy-thirdparty.sh -` - -Type **'yes'** when prompted, if you are running ThingsBoard in `high-availability` `DEPLOYMENT_TYPE` for the first time and don't have configured Redis cluster. - -Execute the following command to deploy resources: - -` -$ ./k8s-deploy-resources.sh -` - -After a while when all resources will be successfully started you can open `http://{your-cluster-ip}` in your browser (for ex. `http://192.168.99.101`). -You should see the ThingsBoard login page. - -Use the following default credentials: - -- **System Administrator**: sysadmin@thingsboard.org / sysadmin - -If you installed DataBase with demo data (using `--loadDemo` flag) you can also use the following credentials: - -- **Tenant Administrator**: tenant@thingsboard.org / tenant -- **Customer User**: customer@thingsboard.org / customer - -In case of any issues, you can examine service logs for errors. -For example to see ThingsBoard node logs execute the following commands: - -1) Get the list of the running tb-node pods: - -` -$ kubectl get pods -l app=tb-node -` - -2) Fetch logs of the tb-node pod: - -` -$ kubectl logs -f [tb-node-pod-name] -` - -Where: - -- `tb-node-pod-name` - tb-node pod name obtained from the list of the running tb-node pods. - -Or use `kubectl get pods` to see the state of all the pods. -Or use `kubectl get services` to see the state of all the services. -Or use `kubectl get deployments` to see the state of all the deployments. -See [kubectl Cheat Sheet](https://kubernetes.io/docs/reference/kubectl/cheatsheet/) command reference for details. - -Execute the following command to delete all ThingsBoard microservices: - -` -$ ./k8s-delete-resources.sh -` - -Execute the following command to delete all third-party microservices: - -` -$ ./k8s-delete-thirdparty.sh -` - -Execute the following command to delete all resources (including database): - -` -$ ./k8s-delete-all.sh -` - -## Upgrading - -In case when database upgrade is needed, execute the following commands: - -``` -$ ./k8s-delete-resources.sh -$ ./k8s-upgrade-tb.sh --fromVersion=[FROM_VERSION] -$ ./k8s-deploy-resources.sh -``` - -Where: - -- `FROM_VERSION` - from which version upgrade should be started. See [Upgrade Instructions](https://thingsboard.io/docs/user-guide/install/upgrade-instructions) for valid `fromVersion` values. diff --git a/k8s/basic/tb-node-cache-configmap.yml b/k8s/basic/tb-node-cache-configmap.yml deleted file mode 100644 index 3a81044f0a..0000000000 --- a/k8s/basic/tb-node-cache-configmap.yml +++ /dev/null @@ -1,26 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-node-cache-config - namespace: thingsboard - labels: - name: tb-node-cache-config -data: - CACHE_TYPE: redis - REDIS_HOST: tb-redis \ No newline at end of file diff --git a/k8s/basic/thirdparty.yml b/k8s/basic/thirdparty.yml deleted file mode 100644 index edba32532e..0000000000 --- a/k8s/basic/thirdparty.yml +++ /dev/null @@ -1,181 +0,0 @@ -# -# 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. -# - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: zookeeper - namespace: thingsboard -spec: - selector: - matchLabels: - app: zookeeper - template: - metadata: - labels: - app: zookeeper - spec: - containers: - - name: server - imagePullPolicy: Always - image: zookeeper:3.5 - ports: - - containerPort: 2181 - readinessProbe: - periodSeconds: 5 - tcpSocket: - port: 2181 - livenessProbe: - initialDelaySeconds: 15 - periodSeconds: 5 - tcpSocket: - port: 2181 - env: - - name: ZOO_MY_ID - value: "1" - - name: ZOO_SERVERS - value: "server.1=0.0.0.0:2888:3888;0.0.0.0:2181" - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: zookeeper - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: zookeeper - ports: - - name: zk-port - port: 2181 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-kafka - namespace: thingsboard -spec: - selector: - matchLabels: - app: tb-kafka - template: - metadata: - labels: - app: tb-kafka - spec: - containers: - - name: server - imagePullPolicy: Always - image: wurstmeister/kafka:2.12-2.2.1 - ports: - - containerPort: 9092 - readinessProbe: - periodSeconds: 20 - tcpSocket: - port: 9092 - livenessProbe: - initialDelaySeconds: 25 - periodSeconds: 5 - tcpSocket: - port: 9092 - env: - - name: KAFKA_ZOOKEEPER_CONNECT - value: "zookeeper:2181" - - name: KAFKA_LISTENERS - value: "INSIDE://:9093,OUTSIDE://:9092" - - name: KAFKA_ADVERTISED_LISTENERS - value: "INSIDE://:9093,OUTSIDE://tb-kafka:9092" - - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP - value: "INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT" - - name: KAFKA_INTER_BROKER_LISTENER_NAME - value: "INSIDE" - - name: KAFKA_CREATE_TOPICS - value: "js_eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_rule_engine:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600" - - name: KAFKA_AUTO_CREATE_TOPICS_ENABLE - value: "false" - - name: KAFKA_LOG_RETENTION_BYTES - value: "1073741824" - - name: KAFKA_LOG_SEGMENT_BYTES - value: "268435456" - - name: KAFKA_LOG_RETENTION_MS - value: "300000" - - name: KAFKA_LOG_CLEANUP_POLICY - value: "delete" - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-kafka - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: tb-kafka - ports: - - name: tb-kafka-port - port: 9092 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-redis - namespace: thingsboard -spec: - selector: - matchLabels: - app: tb-redis - template: - metadata: - labels: - app: tb-redis - spec: - containers: - - name: server - imagePullPolicy: Always - image: redis:4.0 - ports: - - containerPort: 6379 - readinessProbe: - periodSeconds: 5 - tcpSocket: - port: 6379 - livenessProbe: - periodSeconds: 5 - tcpSocket: - port: 6379 - volumeMounts: - - mountPath: /data - name: redis-data - volumes: - - name: redis-data - emptyDir: {} - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-redis - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: tb-redis - ports: - - name: tb-redis-port - port: 6379 ---- \ No newline at end of file diff --git a/k8s/common/cassandra.yml b/k8s/common/cassandra.yml deleted file mode 100644 index 056851242d..0000000000 --- a/k8s/common/cassandra.yml +++ /dev/null @@ -1,164 +0,0 @@ -# -# 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. -# - -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: fast - namespace: thingsboard -provisioner: k8s.io/minikube-hostpath -parameters: - type: pd-ssd ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: cassandra-probe-config - namespace: thingsboard - labels: - name: cassandra-probe-config -data: - probe: | - if [[ $(nodetool status | grep $POD_IP) == *"UN"* ]]; then - if [[ $DEBUG ]]; then - echo "UN"; - fi - exit 0; - else - if [[ $DEBUG ]]; then - echo "Not Up"; - fi - exit 1; - fi ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: cassandra - namespace: thingsboard - labels: - app: cassandra -spec: - serviceName: cassandra - replicas: 1 - selector: - matchLabels: - app: cassandra - template: - metadata: - labels: - app: cassandra - spec: - volumes: - - name: cassandra-probe-config - configMap: - name: cassandra-probe-config - items: - - key: probe - path: ready-probe.sh - mode: 0777 - terminationGracePeriodSeconds: 1800 - containers: - - name: cassandra - image: cassandra:3.11.3 - imagePullPolicy: Always - ports: - - containerPort: 7000 - name: intra-node - - containerPort: 7001 - name: tls-intra-node - - containerPort: 7199 - name: jmx - - containerPort: 9042 - name: cql - - containerPort: 9160 - name: thrift - resources: - limits: - cpu: "1000m" - memory: 2Gi - requests: - cpu: "1000m" - memory: 2Gi - securityContext: - capabilities: - add: - - IPC_LOCK - lifecycle: - preStop: - exec: - command: - - /bin/sh - - -c - - nodetool drain - env: - - name: CASSANDRA_SEEDS - value: "cassandra-0.cassandra.thingsboard.svc.cluster.local" - - name: MAX_HEAP_SIZE - value: 1024M - - name: HEAP_NEWSIZE - value: 256M - - name: CASSANDRA_CLUSTER_NAME - value: "Thingsboard Cluster" - - name: CASSANDRA_DC - value: "datacenter1" - - name: CASSANDRA_RACK - value: "Rack-Thingsboard-Cluster" - - name: CASSANDRA_AUTO_BOOTSTRAP - value: "false" - - name: CASSANDRA_ENDPOINT_SNITCH - value: GossipingPropertyFileSnitch - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - readinessProbe: - exec: - command: - - /bin/bash - - -c - - /probe/ready-probe.sh - initialDelaySeconds: 60 - timeoutSeconds: 5 - volumeMounts: - - name: cassandra-probe-config - mountPath: /probe - - name: cassandra-data - mountPath: /var/lib/cassandra - volumeClaimTemplates: - - metadata: - name: cassandra-data - spec: - accessModes: [ "ReadWriteOnce" ] - storageClassName: fast - resources: - requests: - storage: 1Gi ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: cassandra - name: cassandra - namespace: thingsboard -spec: - clusterIP: None - ports: - - port: 9042 - selector: - app: cassandra ---- diff --git a/k8s/common/database-setup.yml b/k8s/common/database-setup.yml deleted file mode 100644 index 1c7cd48a1b..0000000000 --- a/k8s/common/database-setup.yml +++ /dev/null @@ -1,43 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: Pod -metadata: - name: tb-db-setup - namespace: thingsboard -spec: - volumes: - - name: tb-node-config - configMap: - name: tb-node-config - items: - - key: conf - path: thingsboard.conf - - key: logback - path: logback.xml - containers: - - name: tb-db-setup - imagePullPolicy: Always - image: thingsboard/tb-node:latest - envFrom: - - configMapRef: - name: tb-node-db-config - volumeMounts: - - mountPath: /config - name: tb-node-config - command: ['sh', '-c', 'while [ ! -f /tmp/install-finished ]; do sleep 2; done;'] - restartPolicy: Never diff --git a/k8s/common/postgres.yml b/k8s/common/postgres.yml deleted file mode 100644 index 40ed35d066..0000000000 --- a/k8s/common/postgres.yml +++ /dev/null @@ -1,102 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: postgres-pv-claim - namespace: thingsboard - labels: - app: postgres -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: postgres - namespace: thingsboard - labels: - app: postgres -spec: - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - volumes: - - name: postgres-data - persistentVolumeClaim: - claimName: postgres-pv-claim - containers: - - name: postgres - imagePullPolicy: Always - image: postgres:12 - ports: - - containerPort: 5432 - name: postgres - env: - - name: POSTGRES_DB - value: "thingsboard" - - name: POSTGRES_PASSWORD - value: "postgres" - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - volumeMounts: - - mountPath: /var/lib/postgresql/data - name: postgres-data - livenessProbe: - exec: - command: - - pg_isready - - -h - - localhost - - -U - - postgres - initialDelaySeconds: 60 - timeoutSeconds: 30 - readinessProbe: - exec: - command: - - pg_isready - - -h - - localhost - - -U - - postgres - initialDelaySeconds: 5 - timeoutSeconds: 1 - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-database - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: postgres - ports: - - port: 5432 - name: postgres ---- diff --git a/k8s/common/tb-coap-transport-configmap.yml b/k8s/common/tb-coap-transport-configmap.yml deleted file mode 100644 index 0f0ea0a8e5..0000000000 --- a/k8s/common/tb-coap-transport-configmap.yml +++ /dev/null @@ -1,65 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-coap-transport-config - namespace: thingsboard - labels: - name: tb-coap-transport-config -data: - conf: | - export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-coap-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" - export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-coap-transport/${TB_SERVICE_ID}-heapdump.bin" - export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" - export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" - export LOG_FILENAME=tb-coap-transport.out - export LOADER_PATH=/usr/share/tb-coap-transport/conf - logback: | - - - - - /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.log - - /var/log/tb-coap-transport/${TB_SERVICE_ID}/tb-coap-transport.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - diff --git a/k8s/common/tb-http-transport-configmap.yml b/k8s/common/tb-http-transport-configmap.yml deleted file mode 100644 index 582df2e5e8..0000000000 --- a/k8s/common/tb-http-transport-configmap.yml +++ /dev/null @@ -1,65 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-http-transport-config - namespace: thingsboard - labels: - name: tb-http-transport-config -data: - conf: | - export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-http-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" - export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-http-transport/${TB_SERVICE_ID}-heapdump.bin" - export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" - export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" - export LOG_FILENAME=tb-http-transport.out - export LOADER_PATH=/usr/share/tb-http-transport/conf - logback: | - - - - - /var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.log - - /var/log/tb-http-transport/${TB_SERVICE_ID}/tb-http-transport.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - diff --git a/k8s/common/tb-mqtt-transport-configmap.yml b/k8s/common/tb-mqtt-transport-configmap.yml deleted file mode 100644 index 0d82938842..0000000000 --- a/k8s/common/tb-mqtt-transport-configmap.yml +++ /dev/null @@ -1,65 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-mqtt-transport-config - namespace: thingsboard - labels: - name: tb-mqtt-transport-config -data: - conf: | - export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" - export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tb-mqtt-transport/${TB_SERVICE_ID}-heapdump.bin" - export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" - export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" - export LOG_FILENAME=tb-mqtt-transport.out - export LOADER_PATH=/usr/share/tb-mqtt-transport/conf - logback: | - - - - - /var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.log - - /var/log/tb-mqtt-transport/${TB_SERVICE_ID}/tb-mqtt-transport.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - diff --git a/k8s/common/tb-namespace.yml b/k8s/common/tb-namespace.yml deleted file mode 100644 index c2056c7dd8..0000000000 --- a/k8s/common/tb-namespace.yml +++ /dev/null @@ -1,22 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: Namespace -metadata: - name: thingsboard - labels: - name: thingsboard diff --git a/k8s/common/tb-node-configmap.yml b/k8s/common/tb-node-configmap.yml deleted file mode 100644 index 3e796215ab..0000000000 --- a/k8s/common/tb-node-configmap.yml +++ /dev/null @@ -1,67 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-node-config - namespace: thingsboard - labels: - name: tb-node-config -data: - conf: | - export JAVA_OPTS="$JAVA_OPTS -Dplatform=deb -Dinstall.data_dir=/usr/share/thingsboard/data" - export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/thingsboard/${TB_SERVICE_ID}-gc.log:time,uptime,level,tags:filecount=10,filesize=10M" - export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/thingsboard/${TB_SERVICE_ID}-heapdump.bin" - export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" - export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" - export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" - export LOG_FILENAME=thingsboard.out - export LOADER_PATH=/usr/share/thingsboard/conf,/usr/share/thingsboard/extensions - logback: | - - - - - /var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.log - - /var/log/thingsboard/${TB_SERVICE_ID}/thingsboard.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - - - diff --git a/k8s/common/tb-node-hybrid-configmap.yml b/k8s/common/tb-node-hybrid-configmap.yml deleted file mode 100644 index 0ef6fc4dfb..0000000000 --- a/k8s/common/tb-node-hybrid-configmap.yml +++ /dev/null @@ -1,32 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-node-db-config - namespace: thingsboard - labels: - name: tb-node-db-config -data: - SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect - SPRING_DRIVER_CLASS_NAME: org.postgresql.Driver - SPRING_DATASOURCE_URL: jdbc:postgresql://tb-database:5432/thingsboard - SPRING_DATASOURCE_USERNAME: postgres - SPRING_DATASOURCE_PASSWORD: postgres - DATABASE_TS_TYPE: cassandra - CASSANDRA_URL: cassandra:9042 - CASSANDRA_SOCKET_READ_TIMEOUT: "60000" diff --git a/k8s/common/tb-node-postgres-configmap.yml b/k8s/common/tb-node-postgres-configmap.yml deleted file mode 100644 index 63cc0cb42c..0000000000 --- a/k8s/common/tb-node-postgres-configmap.yml +++ /dev/null @@ -1,30 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-node-db-config - namespace: thingsboard - labels: - name: tb-node-db-config -data: - DATABASE_TS_TYPE: sql - SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect - SPRING_DRIVER_CLASS_NAME: org.postgresql.Driver - SPRING_DATASOURCE_URL: jdbc:postgresql://tb-database:5432/thingsboard - SPRING_DATASOURCE_USERNAME: postgres - SPRING_DATASOURCE_PASSWORD: postgres diff --git a/k8s/common/tb-node.yml b/k8s/common/tb-node.yml deleted file mode 100644 index 2520bf8e76..0000000000 --- a/k8s/common/tb-node.yml +++ /dev/null @@ -1,98 +0,0 @@ -# -# 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. -# - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-node - namespace: thingsboard -spec: - replicas: 2 - selector: - matchLabels: - app: tb-node - template: - metadata: - labels: - app: tb-node - spec: - volumes: - - name: tb-node-config - configMap: - name: tb-node-config - items: - - key: conf - path: thingsboard.conf - - key: logback - path: logback.xml - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-node:latest - ports: - - containerPort: 8080 - name: http - - containerPort: 9001 - name: rpc - env: - - name: TB_SERVICE_ID - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: TB_SERVICE_TYPE - value: "monolith" - - name: TB_QUEUE_TYPE - value: "kafka" - - name: ZOOKEEPER_ENABLED - value: "true" - - name: ZOOKEEPER_URL - value: "zookeeper:2181" - - name: TB_KAFKA_SERVERS - value: "tb-kafka:9092" - - name: JS_EVALUATOR - value: "remote" - - name: TRANSPORT_TYPE - value: "remote" - - name: HTTP_LOG_CONTROLLER_ERROR_STACK_TRACE - value: "false" - envFrom: - - configMapRef: - name: tb-node-db-config - - configMapRef: - name: tb-node-cache-config - volumeMounts: - - mountPath: /config - name: tb-node-config - livenessProbe: - httpGet: - path: /login - port: http - initialDelaySeconds: 300 - timeoutSeconds: 10 - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-node - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: tb-node - ports: - - port: 8080 - name: http \ No newline at end of file diff --git a/k8s/common/thingsboard.yml b/k8s/common/thingsboard.yml deleted file mode 100644 index ee360c27b7..0000000000 --- a/k8s/common/thingsboard.yml +++ /dev/null @@ -1,447 +0,0 @@ -# -# 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. -# - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-js-executor - namespace: thingsboard -spec: - replicas: 20 - selector: - matchLabels: - app: tb-js-executor - template: - metadata: - labels: - app: tb-js-executor - spec: - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-js-executor:latest - env: - - name: REMOTE_JS_EVAL_REQUEST_TOPIC - value: "js_eval.requests" - - name: TB_KAFKA_SERVERS - value: "tb-kafka:9092" - - name: LOGGER_LEVEL - value: "info" - - name: LOG_FOLDER - value: "logs" - - name: LOGGER_FILENAME - value: "tb-js-executor-%DATE%.log" - - name: DOCKER_MODE - value: "true" - - name: SCRIPT_BODY_TRACE_FREQUENCY - value: "1000" - restartPolicy: Always ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-mqtt-transport - namespace: thingsboard -spec: - replicas: 2 - selector: - matchLabels: - app: tb-mqtt-transport - template: - metadata: - labels: - app: tb-mqtt-transport - spec: - volumes: - - name: tb-mqtt-transport-config - configMap: - name: tb-mqtt-transport-config - items: - - key: conf - path: tb-mqtt-transport.conf - - key: logback - path: logback.xml - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-mqtt-transport:latest - ports: - - containerPort: 1883 - name: mqtt - env: - - name: TB_SERVICE_ID - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: TB_QUEUE_TYPE - value: "kafka" - - name: MQTT_BIND_ADDRESS - value: "0.0.0.0" - - name: MQTT_BIND_PORT - value: "1883" - - name: MQTT_TIMEOUT - value: "10000" - - name: TB_KAFKA_SERVERS - value: "tb-kafka:9092" - volumeMounts: - - mountPath: /config - name: tb-mqtt-transport-config - readinessProbe: - periodSeconds: 20 - tcpSocket: - port: 1883 - livenessProbe: - initialDelaySeconds: 120 - periodSeconds: 20 - tcpSocket: - port: 1883 - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-mqtt-transport - namespace: thingsboard -spec: - type: LoadBalancer - selector: - app: tb-mqtt-transport - ports: - - port: 1883 - targetPort: 1883 - name: mqtt ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-http-transport - namespace: thingsboard -spec: - replicas: 2 - selector: - matchLabels: - app: tb-http-transport - template: - metadata: - labels: - app: tb-http-transport - spec: - volumes: - - name: tb-http-transport-config - configMap: - name: tb-http-transport-config - items: - - key: conf - path: tb-http-transport.conf - - key: logback - path: logback.xml - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-http-transport:latest - ports: - - containerPort: 8080 - name: http - env: - - name: TB_SERVICE_ID - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: TB_QUEUE_TYPE - value: "kafka" - - name: HTTP_BIND_ADDRESS - value: "0.0.0.0" - - name: HTTP_BIND_PORT - value: "8080" - - name: HTTP_REQUEST_TIMEOUT - value: "60000" - - name: TB_KAFKA_SERVERS - value: "tb-kafka:9092" - volumeMounts: - - mountPath: /config - name: tb-http-transport-config - readinessProbe: - periodSeconds: 20 - tcpSocket: - port: 8080 - livenessProbe: - initialDelaySeconds: 120 - periodSeconds: 20 - tcpSocket: - port: 8080 - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-http-transport - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: tb-http-transport - ports: - - port: 8080 - name: http ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-coap-transport - namespace: thingsboard -spec: - replicas: 2 - selector: - matchLabels: - app: tb-coap-transport - template: - metadata: - labels: - app: tb-coap-transport - spec: - volumes: - - name: tb-coap-transport-config - configMap: - name: tb-coap-transport-config - items: - - key: conf - path: tb-coap-transport.conf - - key: logback - path: logback.xml - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-coap-transport:latest - ports: - - containerPort: 5683 - name: coap - protocol: UDP - env: - - name: TB_SERVICE_ID - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: TB_QUEUE_TYPE - value: "kafka" - - name: COAP_BIND_ADDRESS - value: "0.0.0.0" - - name: COAP_BIND_PORT - value: "5683" - - name: COAP_TIMEOUT - value: "10000" - - name: TB_KAFKA_SERVERS - value: "tb-kafka:9092" - volumeMounts: - - mountPath: /config - name: tb-coap-transport-config - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-coap-transport - namespace: thingsboard -spec: - type: LoadBalancer - selector: - app: tb-coap-transport - ports: - - port: 5683 - name: coap - protocol: UDP ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-lwm2m-transport - namespace: thingsboard -spec: - replicas: 2 - selector: - matchLabels: - app: tb-lwm2m-transport - template: - metadata: - labels: - app: tb-lwm2m-transport - spec: - volumes: - - name: tb-lwm2m-transport-config - configMap: - name: tb-lwm2m-transport-config - items: - - key: conf - path: tb-lwm2m-transport.conf - - key: logback - path: logback.xml - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-lwm2m-transport:latest - ports: - - containerPort: 5685 - name: lwm2m - protocol: UDP - env: - - name: TB_SERVICE_ID - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: TB_QUEUE_TYPE - value: "kafka" - - name: LWM2M_BIND_ADDRESS - value: "0.0.0.0" - - name: LWM2M_BIND_PORT - value: "5685" - - name: LWM2M_TIMEOUT - value: "10000" - - name: TB_KAFKA_SERVERS - value: "tb-kafka:9092" - volumeMounts: - - mountPath: /config - name: tb-lwm2m-transport-config - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-lwm2m-transport - namespace: thingsboard -spec: - type: LoadBalancer - selector: - app: tb-lwm2m-transport - ports: - - port: 5685 - name: lwm2m - protocol: UDP ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: tb-web-ui - namespace: thingsboard -spec: - replicas: 2 - selector: - matchLabels: - app: tb-web-ui - template: - metadata: - labels: - app: tb-web-ui - spec: - containers: - - name: server - imagePullPolicy: Always - image: thingsboard/tb-web-ui:latest - ports: - - containerPort: 8080 - name: http - env: - - name: HTTP_BIND_ADDRESS - value: "0.0.0.0" - - name: HTTP_BIND_PORT - value: "8080" - - name: TB_ENABLE_PROXY - value: "false" - - name: LOGGER_LEVEL - value: "info" - - name: LOG_FOLDER - value: "logs" - - name: LOGGER_FILENAME - value: "tb-web-ui-%DATE%.log" - - name: DOCKER_MODE - value: "true" - livenessProbe: - httpGet: - path: /index.html - port: http - initialDelaySeconds: 120 - timeoutSeconds: 10 - restartPolicy: Always ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-web-ui - namespace: thingsboard -spec: - type: ClusterIP - selector: - app: tb-web-ui - ports: - - port: 8080 - name: http ---- -apiVersion: networking.k8s.io/v1beta1 -kind: Ingress -metadata: - name: tb-ingress - namespace: thingsboard - annotations: - nginx.ingress.kubernetes.io/use-regex: "true" - nginx.ingress.kubernetes.io/ssl-redirect: "false" - nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" -spec: - rules: - - http: - paths: - - path: /api/v1/.* - backend: - serviceName: tb-http-transport - servicePort: 8080 - - path: /api/.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /swagger.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /webjars.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /v2/.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /static/rulenode/.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /assets/help/.*/rulenode/.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /oauth2/.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: /login/oauth2/.* - backend: - serviceName: tb-node - servicePort: 8080 - - path: / - backend: - serviceName: tb-web-ui - servicePort: 8080 - - path: /.* - backend: - serviceName: tb-web-ui - servicePort: 8080 ---- diff --git a/k8s/high-availability/tb-node-cache-configmap.yml b/k8s/high-availability/tb-node-cache-configmap.yml deleted file mode 100644 index 46800e4fe8..0000000000 --- a/k8s/high-availability/tb-node-cache-configmap.yml +++ /dev/null @@ -1,27 +0,0 @@ -# -# 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. -# - -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-node-cache-config - namespace: thingsboard - labels: - name: tb-node-cache-config -data: - CACHE_TYPE: redis - REDIS_CONNECTION_TYPE: cluster - REDIS_NODES: tb-redis:6379 \ No newline at end of file diff --git a/k8s/high-availability/thirdparty.yml b/k8s/high-availability/thirdparty.yml deleted file mode 100644 index 61f1fa73ce..0000000000 --- a/k8s/high-availability/thirdparty.yml +++ /dev/null @@ -1,301 +0,0 @@ -# -# 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. -# - -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: zookeeper - namespace: thingsboard -spec: - serviceName: "zookeeper" - replicas: 3 - podManagementPolicy: Parallel - selector: - matchLabels: - app: zookeeper - template: - metadata: - labels: - app: zookeeper - spec: - containers: - - name: zookeeper - imagePullPolicy: Always - image: zookeeper:3.5 - ports: - - containerPort: 2181 - name: client - - containerPort: 2888 - name: server - - containerPort: 3888 - name: election - readinessProbe: - periodSeconds: 60 - tcpSocket: - port: 2181 - livenessProbe: - periodSeconds: 60 - tcpSocket: - port: 2181 - env: - - name: ZOO_SERVERS - value: "server.0=zookeeper-0.zookeeper:2888:3888;2181 server.1=zookeeper-1.zookeeper:2888:3888;2181 server.2=zookeeper-2.zookeeper:2888:3888;2181" - - name: JVMFLAGS - value: "-Dzookeeper.electionPortBindRetry=0" - volumeMounts: - - name: data - mountPath: /data - readOnly: false - initContainers: - - command: - - /bin/bash - - -c - - |- - set -ex; - mkdir -p "$ZOO_DATA_LOG_DIR" "$ZOO_DATA_DIR" "$ZOO_CONF_DIR"; - chown "$ZOO_USER:$ZOO_USER" "$ZOO_DATA_LOG_DIR" "$ZOO_DATA_DIR" "$ZOO_CONF_DIR" - if [[ ! -f "$ZOO_DATA_DIR/myid" ]]; then - echo $HOSTNAME| rev | cut -d "-" -f1 | rev > "$ZOO_DATA_DIR/myid" - fi - env: - - name: HOSTNAME - valueFrom: - fieldRef: - fieldPath: metadata.name - image: zookeeper:3.5 - imagePullPolicy: IfNotPresent - name: zookeeper-init - securityContext: - runAsUser: 0 - volumeMounts: - - name: data - mountPath: /data - readOnly: false - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: [ "ReadWriteOnce" ] - resources: - requests: - storage: 100Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: zookeeper - namespace: thingsboard -spec: - type: ClusterIP - ports: - - port: 2181 - targetPort: 2181 - name: client - - port: 2888 - targetPort: 2888 - name: server - - port: 3888 - targetPort: 3888 - name: election - selector: - app: zookeeper ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: tb-kafka - namespace: thingsboard -spec: - serviceName: "tb-kafka" - replicas: 3 - podManagementPolicy: Parallel - selector: - matchLabels: - app: tb-kafka - template: - metadata: - labels: - app: tb-kafka - spec: - containers: - - name: tb-kafka - imagePullPolicy: Always - image: wurstmeister/kafka:2.12-2.2.1 - ports: - - containerPort: 9092 - name: kafka-int - readinessProbe: - periodSeconds: 5 - timeoutSeconds: 5 - tcpSocket: - port: 9092 - initialDelaySeconds: 60 - livenessProbe: - timeoutSeconds: 5 - periodSeconds: 5 - tcpSocket: - port: 9092 - initialDelaySeconds: 80 - env: - - name: BROKER_ID_COMMAND - value: "hostname | cut -d'-' -f3" - - name: KAFKA_ZOOKEEPER_CONNECT - value: "zookeeper:2181" - - name: KAFKA_ZOOKEEPER_CONNECTION_TIMEOUT_MS - value: "60000" - - name: KAFKA_UNCLEAN_LEADER_ELECTION_ENABLE - value: "true" - - name: KAFKA_LISTENERS - value: "INSIDE://:9092" - - name: KAFKA_ADVERTISED_LISTENERS - value: "INSIDE://:9092" - - name: KAFKA_LISTENER_SECURITY_PROTOCOL_MAP - value: "INSIDE:PLAINTEXT" - - name: KAFKA_INTER_BROKER_LISTENER_NAME - value: "INSIDE" - - name: KAFKA_CONTROLLER_SHUTDOWN_ENABLE - value: "true" - - name: KAFKA_CREATE_TOPICS - value: "js_eval.requests:100:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_transport.api.requests:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600,tb_rule_engine:30:1:delete --config=retention.ms=60000 --config=segment.bytes=26214400 --config=retention.bytes=104857600" - - name: KAFKA_AUTO_CREATE_TOPICS_ENABLE - value: "false" - - name: KAFKA_LOG_RETENTION_BYTES - value: "1073741824" - - name: KAFKA_LOG_SEGMENT_BYTES - value: "268435456" - - name: KAFKA_LOG_RETENTION_MS - value: "300000" - - name: KAFKA_LOG_CLEANUP_POLICY - value: "delete" - - name: KAFKA_PORT - value: "9092" - - name: KAFKA_LOG_DIRS - value: "/kafka-logs" - volumeMounts: - - name: logs - mountPath: /kafka-logs - subPath: logs - volumeClaimTemplates: - - metadata: - name: logs - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-kafka - namespace: thingsboard -spec: - type: ClusterIP - ports: - - port: 9092 - targetPort: 9092 - name: kafka-int - selector: - app: tb-kafka ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: tb-redis - namespace: thingsboard -data: - update-node.sh: | - #!/bin/sh - REDIS_NODES="/data/nodes.conf" - sed -i -e "/myself/ s/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/${POD_IP}/" ${REDIS_NODES} - exec "$@" - redis.conf: |+ - cluster-enabled yes - cluster-require-full-coverage no - cluster-node-timeout 15000 - cluster-config-file /data/nodes.conf - cluster-migration-barrier 1 - appendonly yes - protected-mode no ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: tb-redis - namespace: thingsboard -spec: - serviceName: server - replicas: 6 - selector: - matchLabels: - app: tb-redis - template: - metadata: - labels: - app: tb-redis - spec: - containers: - - name: redis - image: redis:5.0.1-alpine - ports: - - containerPort: 6379 - name: client - - containerPort: 16379 - name: gossip - command: ["/conf/update-node.sh", "redis-server", "/conf/redis.conf"] - env: - - name: POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - volumeMounts: - - name: conf - mountPath: /conf - readOnly: false - - name: data - mountPath: /data - readOnly: false - volumes: - - name: conf - configMap: - name: tb-redis - defaultMode: 0755 - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: [ "ReadWriteOnce" ] - resources: - requests: - storage: 100Mi ---- -apiVersion: v1 -kind: Service -metadata: - name: tb-redis - namespace: thingsboard -spec: - type: ClusterIP - ports: - - port: 6379 - targetPort: 6379 - name: client - - port: 16379 - targetPort: 16379 - name: gossip - selector: - app: tb-redis diff --git a/k8s/k8s-delete-all.sh b/k8s/k8s-delete-all.sh deleted file mode 100755 index 75dbd6fa60..0000000000 --- a/k8s/k8s-delete-all.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# -# 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. -# - -kubectl -n thingsboard delete svc,sts,deploy,cm,po,ing --all - -kubectl -n thingsboard get pvc --no-headers=true | awk '//{print $1}' | xargs kubectl -n thingsboard delete --ignore-not-found=true pvc \ No newline at end of file diff --git a/k8s/k8s-delete-resources.sh b/k8s/k8s-delete-resources.sh deleted file mode 100755 index 3dea9a465d..0000000000 --- a/k8s/k8s-delete-resources.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# -# 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. -# - -set -e - -source .env - -kubectl config set-context $(kubectl config current-context) --namespace=thingsboard - -kubectl delete -f common/thingsboard.yml -kubectl delete -f common/tb-node.yml diff --git a/k8s/k8s-delete-thirdparty.sh b/k8s/k8s-delete-thirdparty.sh deleted file mode 100755 index fc5fd3ce16..0000000000 --- a/k8s/k8s-delete-thirdparty.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# -# 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. -# - -set -e - -source .env - -kubectl config set-context $(kubectl config current-context) --namespace=thingsboard -kubectl delete -f $DEPLOYMENT_TYPE/thirdparty.yml diff --git a/k8s/k8s-deploy-resources.sh b/k8s/k8s-deploy-resources.sh deleted file mode 100755 index 899b2e399d..0000000000 --- a/k8s/k8s-deploy-resources.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# -# 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. -# - -set -e - -source .env - -kubectl apply -f common/tb-namespace.yml -kubectl config set-context $(kubectl config current-context) --namespace=thingsboard -kubectl apply -f common/tb-node-configmap.yml -kubectl apply -f common/tb-mqtt-transport-configmap.yml -kubectl apply -f common/tb-http-transport-configmap.yml -kubectl apply -f common/tb-coap-transport-configmap.yml -kubectl apply -f common/thingsboard.yml -kubectl apply -f $DEPLOYMENT_TYPE/tb-node-cache-configmap.yml -kubectl apply -f common/tb-node.yml diff --git a/k8s/k8s-deploy-thirdparty.sh b/k8s/k8s-deploy-thirdparty.sh deleted file mode 100755 index 6fd94c3674..0000000000 --- a/k8s/k8s-deploy-thirdparty.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# -# 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. -# - -set -e - -source .env - -kubectl apply -f common/tb-namespace.yml -kubectl config set-context $(kubectl config current-context) --namespace=thingsboard - -kubectl apply -f $DEPLOYMENT_TYPE/thirdparty.yml - - -if [ "$DEPLOYMENT_TYPE" == "high-availability" ]; then - echo -n "waiting for all redis pods to be ready"; - while [[ $(kubectl get pods tb-redis-5 -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}' 2>/dev/null) != "True" ]]; - do - echo -n "." && sleep 5; - done - - if [[ $(kubectl exec -it tb-redis-0 -- redis-cli cluster info 2>&1 | head -n 1) =~ "cluster_state:ok" ]] - then - echo "redis cluster is already configured" - else - echo "starting redis cluster" - redisNodes=$(kubectl get pods -l app=tb-redis -o jsonpath='{range.items[*]}{.status.podIP}:6379 ') - kubectl exec -it tb-redis-0 -- redis-cli --cluster create --cluster-replicas 1 $redisNodes - fi - -fi - diff --git a/k8s/k8s-install-tb.sh b/k8s/k8s-install-tb.sh deleted file mode 100755 index 3dd714bac9..0000000000 --- a/k8s/k8s-install-tb.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# -# 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. -# - -function installTb() { - - loadDemo=$1 - - kubectl apply -f common/tb-node-configmap.yml - kubectl apply -f common/database-setup.yml && - kubectl wait --for=condition=Ready pod/tb-db-setup --timeout=120s && - kubectl exec tb-db-setup -- sh -c 'export INSTALL_TB=true; export LOAD_DEMO='"$loadDemo"'; start-tb-node.sh; touch /tmp/install-finished;' - - kubectl delete pod tb-db-setup - -} - -function installPostgres() { - - kubectl apply -f common/postgres.yml - kubectl apply -f common/tb-node-postgres-configmap.yml - - kubectl rollout status deployment/postgres -} - -function installHybrid() { - - kubectl apply -f common/postgres.yml - kubectl apply -f common/cassandra.yml - kubectl apply -f common/tb-node-hybrid-configmap.yml - - kubectl rollout status deployment/postgres - kubectl rollout status statefulset/cassandra - - kubectl exec -it cassandra-0 -- bash -c "cqlsh -e \ - \"CREATE KEYSPACE IF NOT EXISTS thingsboard \ - WITH replication = { \ - 'class' : 'SimpleStrategy', \ - 'replication_factor' : 1 \ - };\"" -} - -while [[ $# -gt 0 ]] -do -key="$1" - -case $key in - --loadDemo) - LOAD_DEMO=true - shift # past argument - ;; - *) - # unknown option - ;; -esac -shift # past argument or value -done - -if [ "$LOAD_DEMO" == "true" ]; then - loadDemo=true -else - loadDemo=false -fi - -source .env - -kubectl apply -f common/tb-namespace.yml -kubectl config set-context $(kubectl config current-context) --namespace=thingsboard - -case $DEPLOYMENT_TYPE in - basic) - ;; - high-availability) - ;; - *) - echo "Unknown DEPLOYMENT_TYPE value specified: '${DEPLOYMENT_TYPE}'. Should be either basic or high-availability." >&2 - exit 1 -esac - -case $DATABASE in - postgres) - installPostgres - installTb ${loadDemo} - ;; - hybrid) - installHybrid - installTb ${loadDemo} - ;; - *) - echo "Unknown DATABASE value specified: '${DATABASE}'. Should be either postgres or hybrid." >&2 - exit 1 -esac - diff --git a/k8s/k8s-upgrade-tb.sh b/k8s/k8s-upgrade-tb.sh deleted file mode 100755 index 5e81e500a3..0000000000 --- a/k8s/k8s-upgrade-tb.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# -# 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. -# - -for i in "$@" -do -case $i in - --fromVersion=*) - FROM_VERSION="${i#*=}" - shift - ;; - *) - # unknown option - ;; -esac -done - -if [[ -z "${FROM_VERSION// }" ]]; then - echo "--fromVersion parameter is invalid or unspecified!" - echo "Usage: k8s-upgrade-tb.sh --fromVersion={VERSION}" - exit 1 -else - fromVersion="${FROM_VERSION// }" -fi - -kubectl apply -f common/database-setup.yml && -kubectl wait --for=condition=Ready pod/tb-db-setup --timeout=120s && -kubectl exec tb-db-setup -- sh -c 'export UPGRADE_TB=true; export FROM_VERSION='"$fromVersion"'; start-tb-node.sh; touch /tmp/install-finished;' - -kubectl delete pod tb-db-setup diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index b642f5a2c5..5a0645c03a 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 5c9924dd50..9fe84ffa9f 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.3.2", + "version": "3.3.3", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.js", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index f08a17987a..5cabaed58a 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/pom.xml b/msa/pom.xml index af70451a1e..024607378c 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 90b8e4ede6..9cf97b4906 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index f55f5b12e5..841a4edbdf 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index f6e226802a..34b37c6334 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index b14fa3b82f..0cc5320250 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index c02dc88e7e..2dd44c4386 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 9258221754..d483ef42eb 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 3a19267950..06c1d17939 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 7be1e011a2..a30942fe4c 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index b984118482..cc98e5f457 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.3.2", + "version": "3.3.3", "description": "ThingsBoard Web UI Microservice", "main": "server.js", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 0a231344de..2b973afaba 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/server.js b/msa/web-ui/server.js index 5c2c6ac34d..f3561ddb7f 100644 --- a/msa/web-ui/server.js +++ b/msa/web-ui/server.js @@ -83,10 +83,6 @@ var server; apiProxy.web(req, res); }); - app.all('/assets/help/*/rulenode/**', (req, res) => { - apiProxy.web(req, res); - }); - server.on('upgrade', (req, socket, head) => { apiProxy.ws(req, socket, head); }); diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 93ccb59913..e6c2843b14 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard netty-mqtt - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT jar Netty MQTT Client diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index fdc44dbb42..80a336cbb2 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -55,6 +55,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; /** * Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times @@ -155,11 +156,14 @@ final class MqttClientImpl implements MqttClient { if (callback != null) { callback.connectionLost(e); } + pendingSubscriptions.forEach((id, mqttPendingSubscription) -> mqttPendingSubscription.onChannelClosed()); pendingSubscriptions.clear(); serverSubscriptions.clear(); subscriptions.clear(); + pendingServerUnsubscribes.forEach((id, mqttPendingServerUnsubscribes) -> mqttPendingServerUnsubscribes.onChannelClosed()); pendingServerUnsubscribes.clear(); qos2PendingIncomingPublishes.clear(); + pendingPublishes.forEach((id, mqttPendingPublish) -> mqttPendingPublish.onChannelClosed()); pendingPublishes.clear(); pendingSubscribeTopics.clear(); handlerToSubscribtion.clear(); @@ -361,24 +365,30 @@ final class MqttClientImpl implements MqttClient { MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false, qos, retain, 0); MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader(topic, getNewMessageId().messageId()); MqttPublishMessage message = new MqttPublishMessage(fixedHeader, variableHeader, payload); - MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.packetId(), future, payload.retain(), message, qos); + MqttPendingPublish pendingPublish = new MqttPendingPublish(variableHeader.packetId(), future, + payload.retain(), message, qos, () -> !pendingPublishes.containsKey(variableHeader.packetId())); this.pendingPublishes.put(pendingPublish.getMessageId(), pendingPublish); ChannelFuture channelFuture = this.sendAndFlushPacket(message); if (channelFuture != null) { - pendingPublish.setSent(true); - if (channelFuture.cause() != null) { - future.setFailure(channelFuture.cause()); - return future; - } - } - if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { - this.pendingPublishes.remove(pendingPublish.getMessageId()); - pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 - } else if (pendingPublish.isSent()) { - pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); + channelFuture.addListener(result -> { + pendingPublish.setSent(true); + if (result.cause() != null) { + pendingPublishes.remove(pendingPublish.getMessageId()); + future.setFailure(result.cause()); + } else { + if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { + pendingPublishes.remove(pendingPublish.getMessageId()); + pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 + } else if (pendingPublish.isSent()) { + pendingPublish.startPublishRetransmissionTimer(eventLoop.next(), MqttClientImpl.this::sendAndFlushPacket); + } else { + pendingPublishes.remove(pendingPublish.getMessageId()); + } + } + }); } else { - this.pendingPublishes.remove(pendingPublish.getMessageId()); + pendingPublishes.remove(pendingPublish.getMessageId()); } return future; } @@ -462,7 +472,8 @@ final class MqttClientImpl implements MqttClient { MqttSubscribePayload payload = new MqttSubscribePayload(Collections.singletonList(subscription)); MqttSubscribeMessage message = new MqttSubscribeMessage(fixedHeader, variableHeader, payload); - final MqttPendingSubscription pendingSubscription = new MqttPendingSubscription(future, topic, message); + final MqttPendingSubscription pendingSubscription = new MqttPendingSubscription(future, topic, message, + () -> !pendingSubscriptions.containsKey(variableHeader.messageId())); pendingSubscription.addHandler(handler, once); this.pendingSubscriptions.put(variableHeader.messageId(), pendingSubscription); this.pendingSubscribeTopics.add(topic); @@ -480,7 +491,8 @@ final class MqttClientImpl implements MqttClient { MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic)); MqttUnsubscribeMessage message = new MqttUnsubscribeMessage(fixedHeader, variableHeader, payload); - MqttPendingUnsubscription pendingUnsubscription = new MqttPendingUnsubscription(promise, topic, message); + MqttPendingUnsubscription pendingUnsubscription = new MqttPendingUnsubscription(promise, topic, message, + () -> !pendingServerUnsubscribes.containsKey(variableHeader.messageId())); this.pendingServerUnsubscribes.put(variableHeader.messageId(), pendingUnsubscription); pendingUnsubscription.startRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java index cedbd4556d..8369d8f65e 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingPublish.java @@ -32,19 +32,21 @@ final class MqttPendingPublish { private final MqttPublishMessage message; private final MqttQoS qos; - private final RetransmissionHandler publishRetransmissionHandler = new RetransmissionHandler<>(); - private final RetransmissionHandler pubrelRetransmissionHandler = new RetransmissionHandler<>(); + private final RetransmissionHandler publishRetransmissionHandler; + private final RetransmissionHandler pubrelRetransmissionHandler; private boolean sent = false; - MqttPendingPublish(int messageId, Promise future, ByteBuf payload, MqttPublishMessage message, MqttQoS qos) { + MqttPendingPublish(int messageId, Promise future, ByteBuf payload, MqttPublishMessage message, MqttQoS qos, PendingOperation operation) { this.messageId = messageId; this.future = future; this.payload = payload; this.message = message; this.qos = qos; + this.publishRetransmissionHandler = new RetransmissionHandler<>(operation); this.publishRetransmissionHandler.setOriginalMessage(message); + this.pubrelRetransmissionHandler = new RetransmissionHandler<>(operation); } int getMessageId() { @@ -98,4 +100,12 @@ final class MqttPendingPublish { void onPubcompReceived() { this.pubrelRetransmissionHandler.stop(); } + + void onChannelClosed() { + this.publishRetransmissionHandler.stop(); + this.pubrelRetransmissionHandler.stop(); + if (payload != null) { + payload.release(); + } + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscription.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscription.java index d0d396d784..17bfa1139d 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscription.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscription.java @@ -30,15 +30,16 @@ final class MqttPendingSubscription { private final Set handlers = new HashSet<>(); private final MqttSubscribeMessage subscribeMessage; - private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); + private final RetransmissionHandler retransmissionHandler; private boolean sent = false; - MqttPendingSubscription(Promise future, String topic, MqttSubscribeMessage message) { + MqttPendingSubscription(Promise future, String topic, MqttSubscribeMessage message, PendingOperation operation) { this.future = future; this.topic = topic; this.subscribeMessage = message; + this.retransmissionHandler = new RetransmissionHandler<>(operation); this.retransmissionHandler.setOriginalMessage(message); } @@ -62,7 +63,7 @@ final class MqttPendingSubscription { return subscribeMessage; } - void addHandler(MqttHandler handler, boolean once){ + void addHandler(MqttHandler handler, boolean once) { this.handlers.add(new MqttPendingHandler(handler, once)); } @@ -71,14 +72,14 @@ final class MqttPendingSubscription { } void startRetransmitTimer(EventLoop eventLoop, Consumer sendPacket) { - if(this.sent){ //If the packet is sent, we can start the retransmit timer + if (this.sent) { //If the packet is sent, we can start the retransmit timer this.retransmissionHandler.setHandle((fixedHeader, originalMessage) -> sendPacket.accept(new MqttSubscribeMessage(fixedHeader, originalMessage.variableHeader(), originalMessage.payload()))); this.retransmissionHandler.start(eventLoop); } } - void onSubackReceived(){ + void onSubackReceived() { this.retransmissionHandler.stop(); } @@ -99,4 +100,8 @@ final class MqttPendingSubscription { return once; } } + + void onChannelClosed() { + this.retransmissionHandler.stop(); + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscription.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscription.java index ca9d0b6e77..42d2b852b9 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscription.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscription.java @@ -21,17 +21,18 @@ import io.netty.util.concurrent.Promise; import java.util.function.Consumer; -final class MqttPendingUnsubscription { +final class MqttPendingUnsubscription{ private final Promise future; private final String topic; - private final RetransmissionHandler retransmissionHandler = new RetransmissionHandler<>(); + private final RetransmissionHandler retransmissionHandler; - MqttPendingUnsubscription(Promise future, String topic, MqttUnsubscribeMessage unsubscribeMessage) { + MqttPendingUnsubscription(Promise future, String topic, MqttUnsubscribeMessage unsubscribeMessage, PendingOperation operation) { this.future = future; this.topic = topic; + this.retransmissionHandler = new RetransmissionHandler<>(operation); this.retransmissionHandler.setOriginalMessage(unsubscribeMessage); } @@ -52,4 +53,8 @@ final class MqttPendingUnsubscription { void onUnsubackReceived(){ this.retransmissionHandler.stop(); } + + void onChannelClosed(){ + this.retransmissionHandler.stop(); + } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java index 9f06ff84e4..62b5e059f4 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java @@ -28,10 +28,10 @@ final class MqttSubscription { private boolean called; MqttSubscription(String topic, MqttHandler handler, boolean once) { - if(topic == null){ + if (topic == null) { throw new NullPointerException("topic"); } - if(handler == null){ + if (handler == null) { throw new NullPointerException("handler"); } this.topic = topic; @@ -56,7 +56,7 @@ final class MqttSubscription { return called; } - boolean matches(String topic){ + boolean matches(String topic) { return this.topicRegex.matcher(topic).matches(); } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/PendingOperation.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/PendingOperation.java new file mode 100644 index 0000000000..c9ab7c5f5e --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/PendingOperation.java @@ -0,0 +1,22 @@ +/** + * 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.mqtt; + +public interface PendingOperation { + + boolean isCanceled(); + +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java index 531d3bf19a..5d3f9b39d6 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/RetransmissionHandler.java @@ -21,33 +21,43 @@ import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttMessageType; import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.util.concurrent.ScheduledFuture; +import lombok.RequiredArgsConstructor; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; +@RequiredArgsConstructor final class RetransmissionHandler { + private volatile boolean stopped; + private final PendingOperation pendingOperation; private ScheduledFuture timer; private int timeout = 10; private BiConsumer handler; private T originalMessage; - void start(EventLoop eventLoop){ - if(eventLoop == null){ + void start(EventLoop eventLoop) { + if (eventLoop == null) { throw new NullPointerException("eventLoop"); } - if(this.handler == null){ + if (this.handler == null) { throw new NullPointerException("handler"); } this.timeout = 10; this.startTimer(eventLoop); } - private void startTimer(EventLoop eventLoop){ + private void startTimer(EventLoop eventLoop) { + if (stopped || pendingOperation.isCanceled()) { + return; + } this.timer = eventLoop.schedule(() -> { + if (stopped || pendingOperation.isCanceled()) { + return; + } this.timeout += 5; boolean isDup = this.originalMessage.fixedHeader().isDup(); - if(this.originalMessage.fixedHeader().messageType() == MqttMessageType.PUBLISH && this.originalMessage.fixedHeader().qosLevel() != MqttQoS.AT_MOST_ONCE){ + if (this.originalMessage.fixedHeader().messageType() == MqttMessageType.PUBLISH && this.originalMessage.fixedHeader().qosLevel() != MqttQoS.AT_MOST_ONCE) { isDup = true; } MqttFixedHeader fixedHeader = new MqttFixedHeader(this.originalMessage.fixedHeader().messageType(), isDup, this.originalMessage.fixedHeader().qosLevel(), this.originalMessage.fixedHeader().isRetain(), this.originalMessage.fixedHeader().remainingLength()); @@ -56,8 +66,9 @@ final class RetransmissionHandler { }, timeout, TimeUnit.SECONDS); } - void stop(){ - if(this.timer != null){ + void stop() { + stopped = true; + if (this.timer != null) { this.timer.cancel(true); } } diff --git a/pom.xml b/pom.xml index 0ca42294fa..ab92c575fd 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT pom Thingsboard @@ -46,8 +46,8 @@ 2.4.3 3.3.0 0.7.0 - 1.7.7 - 1.2.3 + 1.7.32 + 1.2.6 0.10 4.10.0 4.0.5 @@ -83,9 +83,8 @@ 4.8.0 2.19.1 3.0.2 - 2.6.1 - 1.0.0 - 1.5.10 + 3.0.4 + 1.6.3 0.7 1.15.0 1.67 @@ -122,7 +121,7 @@ 4.1.0 4.3.1.0 2.7.2 - 2.6.0 + 2.6.1 1.5.2 5.6.3 2.6.0 @@ -188,9 +187,17 @@ src/main/resources logback.xml + ${pkg.name}.yml false + + src/main/resources + + ${pkg.name}.yml + + true + @@ -264,9 +271,17 @@ src/main/resources logback.xml + ${pkg.name}.yml false + + src/main/resources + + ${pkg.name}.yml + + true + src/main/conf @@ -815,6 +830,7 @@ src/.browserslistrc **/yarn.lock **/*.raw + **/*.patch **/apache/cassandra/io/** .run/** @@ -1620,8 +1636,8 @@ ${curator.version} - io.springfox - springfox-swagger2 + org.thingsboard + springfox-boot-starter ${springfox-swagger.version} @@ -1699,11 +1715,6 @@ fst ${fst.version} - - io.springfox.ui - springfox-swagger-ui-rfc6570 - ${springfox-swagger-ui-rfc6570.version} - org.locationtech.spatial4j spatial4j diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 87d3b6fa9d..36ec99fab6 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard rest-client diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 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/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java b/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java index 9d17c6d84a..0b28dda9f0 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/utils/RestJsonConverter.java @@ -71,7 +71,7 @@ public class RestJsonConverter { } private static KvEntry parseValue(String key, JsonNode value) { - if (!value.isObject()) { + if (!value.isContainerNode()) { if (value.isBoolean()) { return new BooleanDataEntry(key, value.asBoolean()); } else if (value.isNumber()) { diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 941188b24a..986ac17cae 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 1465ac1fd8..03510ab9ce 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java index 531ac61a2c..5933b1bf18 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java @@ -27,6 +27,7 @@ public class NodeDefinition { private boolean outEnabled; String[] relationTypes; boolean customRelations; + boolean ruleChainNode; JsonNode defaultConfiguration; String[] uiResources; String configDirective; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java index 53b52491da..7a0f99e2d0 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java @@ -58,6 +58,8 @@ public @interface RuleNode { boolean customRelations() default false; + boolean ruleChainNode() default false; + RuleChainType[] ruleChainTypes() default {RuleChainType.CORE, RuleChainType.EDGE}; } 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..206256e190 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 @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -121,6 +122,24 @@ public interface TbContext { */ void enqueue(TbMsg msg, Runnable onSuccess, Consumer onFailure); + /** + * Sends message to the nested rule chain. + * Fails processing of the message if the nested rule chain is not found. + * + * @param msg - the message + * @param ruleChainId - the id of a nested rule chain + */ + void input(TbMsg msg, RuleChainId ruleChainId); + + /** + * Sends message to the caller rule chain. + * Acknowledge the message if no caller rule chain is present in processing stack + * + * @param msg - the message + * @param relationType - the relation type that will be used to route messages in the caller rule chain + */ + void output(TbMsg msg, String relationType); + /** * Puts new message to custom queue for processing * @@ -245,7 +264,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-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index c34d6b3df6..2be6f0cdba 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT rule-engine org.thingsboard.rule-engine 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 24736136f2..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,19 +25,7 @@ public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigurat @Override public TbClearAlarmNodeConfiguration defaultConfiguration() { TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration(); - configuration.setAlarmDetailsBuildJs("" + - "//***DO NOT CHANGE THIS LINES***\n" + - "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" + - "//***PLACE YOUR CODE BELOW***\n" + - "\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/TbCreateRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java index 5c06183106..28eafabfce 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateRelationNode.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.action; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -69,41 +70,31 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity, String relationType) { - ListenableFuture future = createIfAbsent(ctx, msg, entity, relationType); + ListenableFuture future = createRelationIfAbsent(ctx, msg, entity, relationType); return Futures.transform(future, result -> { - RelationContainer container = new RelationContainer(); if (result && config.isChangeOriginatorToRelatedEntity()) { TbMsg tbMsg = ctx.transformMsg(msg, msg.getType(), entity.getEntityId(), msg.getMetaData(), msg.getData()); - container.setMsg(tbMsg); - } else { - container.setMsg(msg); + return new RelationContainer(tbMsg, result); } - container.setResult(result); - return container; + return new RelationContainer(msg, result); }, ctx.getDbCallbackExecutor()); } - private ListenableFuture createIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) { + private ListenableFuture createRelationIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer, String relationType) { SearchDirectionIds sdId = processSingleSearchDirection(msg, entityContainer); - ListenableFuture checkRelationFuture = Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON), result -> { - if (!result) { - if (config.isRemoveCurrentRelations()) { - return processDeleteRelations(ctx, processFindRelations(ctx, msg, sdId, relationType)); - } - return Futures.immediateFuture(false); - } - return Futures.immediateFuture(true); - }, ctx.getDbCallbackExecutor()); + return Futures.transformAsync(deleteCurrentRelationsIfNeeded(ctx, msg, sdId, relationType), v -> + checkRelationAndCreateIfAbsent(ctx, entityContainer, relationType, sdId), + ctx.getDbCallbackExecutor()); + } - return Futures.transformAsync(checkRelationFuture, result -> { - if (!result) { - return processCreateRelation(ctx, entityContainer, sdId, relationType); - } - return Futures.immediateFuture(true); - }, ctx.getDbCallbackExecutor()); + private ListenableFuture deleteCurrentRelationsIfNeeded(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) { + if (config.isRemoveCurrentRelations()) { + return deleteOriginatorRelations(ctx, findOriginatorRelations(ctx, msg, sdId, relationType)); + } + return Futures.immediateFuture(null); } - private ListenableFuture> processFindRelations(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) { + private ListenableFuture> findOriginatorRelations(TbContext ctx, TbMsg msg, SearchDirectionIds sdId, String relationType) { if (sdId.isOriginatorDirectionFrom()) { return ctx.getRelationService().findByFromAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), relationType, RelationTypeGroup.COMMON); } else { @@ -111,19 +102,31 @@ public class TbCreateRelationNode extends TbAbstractRelationActionNode processDeleteRelations(TbContext ctx, ListenableFuture> listListenableFuture) { - return Futures.transformAsync(listListenableFuture, entityRelations -> { - if (!entityRelations.isEmpty()) { - List> list = new ArrayList<>(); - for (EntityRelation relation : entityRelations) { + private ListenableFuture deleteOriginatorRelations(TbContext ctx, ListenableFuture> originatorRelationsFuture) { + return Futures.transformAsync(originatorRelationsFuture, originatorRelations -> { + List> list = new ArrayList<>(); + if (!CollectionUtils.isEmpty(originatorRelations)) { + for (EntityRelation relation : originatorRelations) { list.add(ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation)); } - return Futures.transform(Futures.allAsList(list), result -> false, ctx.getDbCallbackExecutor()); } - return Futures.immediateFuture(false); + return Futures.transform(Futures.allAsList(list), result -> null, ctx.getDbCallbackExecutor()); }, ctx.getDbCallbackExecutor()); } + private ListenableFuture checkRelationAndCreateIfAbsent(TbContext ctx, EntityContainer entityContainer, String relationType, SearchDirectionIds sdId) { + return Futures.transformAsync(checkRelation(ctx, sdId, relationType), relationPresent -> { + if (relationPresent) { + return Futures.immediateFuture(true); + } + return processCreateRelation(ctx, entityContainer, sdId, relationType); + }, ctx.getDbCallbackExecutor()); + } + + private ListenableFuture checkRelation(TbContext ctx, SearchDirectionIds sdId, String relationType) { + return ctx.getRelationService().checkRelation(ctx.getTenantId(), sdId.getFromId(), sdId.getToId(), relationType, RelationTypeGroup.COMMON); + } + private ListenableFuture processCreateRelation(TbContext ctx, EntityContainer entityContainer, SearchDirectionIds sdId, String relationType) { switch (entityContainer.getEntityType()) { case ASSET: 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/flow/TbAckNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java index e10a20232d..850eed19bf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg; @Slf4j @RuleNode( - type = ComponentType.ACTION, + type = ComponentType.FLOW, name = "acknowledge", configClazz = EmptyNodeConfiguration.class, nodeDescription = "Acknowledges the incoming message", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index cc3944452a..4f3bc327ec 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -31,7 +31,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j @RuleNode( - type = ComponentType.ACTION, + type = ComponentType.FLOW, name = "checkpoint", configClazz = TbCheckpointNodeConfiguration.class, nodeDescription = "transfers the message to another queue", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java new file mode 100644 index 0000000000..a1e1e31954 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.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.rule.engine.flow; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; + +import java.util.UUID; + +@Slf4j +@RuleNode( + type = ComponentType.FLOW, + name = "rule chain", + configClazz = TbRuleChainInputNodeConfiguration.class, + nodeDescription = "transfers the message to another rule chain", + nodeDetails = "Allows to nest the rule chain similar to single rule node. " + + "The incoming message is forwarded to the input node of the specified target rule chain. " + + "The target rule chain may produce multiple labeled outputs. " + + "You may use the outputs to forward the results of processing to other rule nodes.", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFlowNodeRuleChainInputConfig", + relationTypes = {}, + ruleChainNode = true, + customRelations = true +) +public class TbRuleChainInputNode implements TbNode { + + private TbRuleChainInputNodeConfiguration config; + private RuleChainId ruleChainId; + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + this.config = TbNodeUtils.convert(configuration, TbRuleChainInputNodeConfiguration.class); + this.ruleChainId = new RuleChainId(UUID.fromString(config.getRuleChainId())); + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + ctx.input(msg, ruleChainId); + } + + @Override + public void destroy() { + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.java new file mode 100644 index 0000000000..b990411216 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNodeConfiguration.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.rule.engine.flow; + +import lombok.Data; +import org.thingsboard.rule.engine.api.NodeConfiguration; +import org.thingsboard.server.common.data.id.RuleChainId; + +@Data +public class TbRuleChainInputNodeConfiguration implements NodeConfiguration { + + private String ruleChainId; + + @Override + public TbRuleChainInputNodeConfiguration defaultConfiguration() { + return new TbRuleChainInputNodeConfiguration(); + } + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java new file mode 100644 index 0000000000..59ddc1c096 --- /dev/null +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java @@ -0,0 +1,57 @@ +/** + * 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.flow; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; +import org.thingsboard.rule.engine.api.RuleNode; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNode; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.common.msg.TbMsg; + +@Slf4j +@RuleNode( + type = ComponentType.FLOW, + name = "output", + configClazz = EmptyNodeConfiguration.class, + nodeDescription = "transfers the message to the caller rule chain", + nodeDetails = "Produces output of the rule chain processing. " + + "The output is forwarded to the caller rule chain, as an output of the corresponding \"input\" rule node. " + + "The output rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain. ", + uiResources = {"static/rulenode/rulenode-core-config.js"}, + configDirective = "tbFlowNodeRuleChainOutputConfig", + outEnabled = false +) +public class TbRuleChainOutputNode implements TbNode { + + @Override + public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { + } + + @Override + public void onMsg(TbContext ctx, TbMsg msg) { + ctx.output(msg, ctx.getSelf().getName()); + } + + @Override + public void destroy() { + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java index b5612dfc6a..a67693749e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/GeoUtil.java @@ -45,24 +45,42 @@ public class GeoUtil { } public static synchronized boolean contains(String polygon, Coordinates coordinates) { - ShapeFactory.PolygonBuilder polygonBuilder = jtsCtx.getShapeFactory().polygon(); JsonArray polygonArray = new JsonParser().parse(polygon).getAsJsonArray(); - boolean first = true; + + JsonArray arrayWithCoords = polygonArray; + JsonArray innerArray = polygonArray.get(0).getAsJsonArray(); + if (!containsPrimitives(innerArray)) { + arrayWithCoords = innerArray; + } + + Shape shape = buildPolygonFromJsonCoords(arrayWithCoords); + Point point = jtsCtx.getShapeFactory().pointXY(coordinates.getLongitude(), coordinates.getLatitude()); + return shape.relate(point).equals(SpatialRelation.CONTAINS); + } + + private static Shape buildPolygonFromJsonCoords(JsonArray coordinates) { + ShapeFactory.PolygonBuilder polygonBuilder = jtsCtx.getShapeFactory().polygon(); + boolean isFirst = true; double firstLat = 0.0; double firstLng = 0.0; - for (JsonElement jsonElement : polygonArray) { - double lat = jsonElement.getAsJsonArray().get(0).getAsDouble(); - double lng = jsonElement.getAsJsonArray().get(1).getAsDouble(); - if (first) { + for (JsonElement element : coordinates) { + double lat = element.getAsJsonArray().get(0).getAsDouble(); + double lng = element.getAsJsonArray().get(1).getAsDouble(); + if (isFirst) { firstLat = lat; firstLng = lng; - first = false; + isFirst = false; } - polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(lng), jtsCtx.getShapeFactory().normY(lat)); + polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(lng), jtsCtx.getShapeFactory().normX(lat)); } - polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(firstLng), jtsCtx.getShapeFactory().normY(firstLat)); - Shape shape = polygonBuilder.buildOrRect(); - Point point = jtsCtx.getShapeFactory().pointXY(coordinates.getLongitude(), coordinates.getLatitude()); - return shape.relate(point).equals(SpatialRelation.CONTAINS); + polygonBuilder.pointXY(jtsCtx.getShapeFactory().normX(firstLng), jtsCtx.getShapeFactory().normX(firstLat)); + return polygonBuilder.buildOrRect(); + } + + private static boolean containsPrimitives(JsonArray array) { + for (JsonElement element : array) { + return element.isJsonPrimitive(); + } + return false; } } 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/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java index e06824180e..9ac797a98a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java @@ -37,7 +37,6 @@ public class TbMqttNodeConfiguration implements NodeConfiguration entity; if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || - HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method)) { + HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) || + config.isIgnoreRequestBody()) { entity = new HttpEntity<>(headers); } else { entity = new HttpEntity<>(msg.getData(), headers); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java index a357efec31..984c6d9972 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java @@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration -
- -*function (msg, metadata msgType): {msg: object, metadata: object, msgType: string}* - -JavaScript function transforming input Message payload, Metadata or Message type. - -**Parameters:** - -
    -
  • msg: {[key: string]: any} - is a Message payload key/value object. -
  • -
  • metadata: {[key: string]: string} - is a Message metadata key/value object. -
  • -
  • msgType: string - is a string Message type. See MessageType enum for common used values. -
  • -
- -**Returns:** - -Should return the object with the following structure: - -```javascript -{ - msg?: {[key: string]: any}, - metadata?: {[key: string]: string}, - msgType?: string -} -``` - -All fields in resulting object are optional and will be taken from original message if not specified. - -
- -##### Examples - -* Change message type to `CUSTOM_REQUEST`: - -```javascript -return { msgType: 'CUSTOM_REQUEST' }; -{:copy-code} -``` - -
    -
  • Change message type to CUSTOM_UPDATE,
    add additional attribute version into payload with value v1.1,
    change sensorType attribute value in Metadata to roomTemp:
  • -
- -The following transform function will perform all necessary modifications: - -```javascript -var newType = "CUSTOM_UPDATE"; -msg.version = "v1.1"; -metadata.sensorType = "roomTemp" -return {msg: msg, metadata: metadata, msgType: newType}; -{:copy-code} -``` - -You can see real life example, how to use this node in those tutorials: - -- [Transform incoming telemetry{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/transform-incoming-telemetry/) -- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-transform-script-node) - - 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 59f54df936..ae61853e4b 100644 --- a/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js +++ b/rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js @@ -1,16 +1,4264 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@angular/core"),require("@angular/common"),require("@ngx-translate/core"),require("@shared/public-api"),require("@ngrx/store"),require("@angular/forms"),require("@home/components/public-api"),require("@core/public-api"),require("@angular/cdk/keycodes"),require("@angular/cdk/coercion"),require("@angular/material/chips"),require("@angular/material/autocomplete"),require("rxjs"),require("rxjs/operators"),require("@angular/platform-browser")):"function"==typeof define&&define.amd?define("rulenode-core-config",["exports","@angular/core","@angular/common","@ngx-translate/core","@shared/public-api","@ngrx/store","@angular/forms","@home/components/public-api","@core/public-api","@angular/cdk/keycodes","@angular/cdk/coercion","@angular/material/chips","@angular/material/autocomplete","rxjs","rxjs/operators","@angular/platform-browser"],t):t((e=e||self)["rulenode-core-config"]={},e.ng.core,e.ng.common,e["ngx-translate"],e.shared,e["ngrx-store"],e.ng.forms,e.publicApi$1,e.core,e.ng.cdk.keycodes,e.ng.cdk.coercion,e.ng.material.chips,e.ng.material.autocomplete,e.rxjs,e.rxjs.operators,e.ng.platformBrowser)}(this,(function(e,t,r,n,a,o,i,l,s,m,u,d,p,c,f,g){"use strict"; -/*! ***************************************************************************** - Copyright (c) Microsoft Corporation. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - 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"}(_||(_={}));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(_),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)}(),_e=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:[_e,Qe,$e],imports:[r.CommonModule,a.SharedModule,ue],exports:[_e,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,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=_e,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 +import * as i0 from '@angular/core'; +import { Component, Pipe, ViewChild, forwardRef, Input, NgModule } from '@angular/core'; +import * as i3$4 from '@shared/public-api'; +import { RuleNodeConfigurationComponent, AttributeScope, telemetryTypeTranslations, ServiceType, AlarmSeverity, alarmSeverityTranslations, EntitySearchDirection, entitySearchDirectionTranslations, EntityType, PageComponent, MessageType, messageTypeNames, SharedModule, AggregationType, aggregationTranslations, alarmStatusTranslations, AlarmStatus } from '@shared/public-api'; +import * as i1 from '@ngrx/store'; +import * as i2 from '@angular/forms'; +import { Validators, NgControl, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl } from '@angular/forms'; +import * as i3 from '@angular/material/form-field'; +import * as i3$1 from '@angular/material/checkbox'; +import * as i8 from '@angular/flex-layout/flex'; +import * as i4 from '@ngx-translate/core'; +import * as i11 from '@angular/material/input'; +import * as i10 from '@angular/common'; +import { CommonModule } from '@angular/common'; +import * as i1$1 from '@angular/platform-browser'; +import * as i4$1 from '@angular/material/select'; +import * as i5 from '@angular/material/core'; +import * as i4$2 from '@angular/material/expansion'; +import * as i7 from '@shared/components/button/toggle-password.component'; +import * as i8$1 from '@shared/components/file-input.component'; +import * as i3$2 from '@shared/components/queue/queue-type-list.component'; +import * as i3$3 from '@core/public-api'; +import { isDefinedAndNotNull } from '@core/public-api'; +import * as i5$1 from '@shared/components/js-func.component'; +import * as i6 from '@angular/material/button'; +import { ENTER, COMMA, SEMICOLON } from '@angular/cdk/keycodes'; +import * as i5$2 from '@angular/material/chips'; +import * as i6$1 from '@angular/material/icon'; +import * as i7$1 from '@shared/components/entity/entity-type-select.component'; +import * as i6$2 from '@shared/components/entity/entity-select.component'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import * as i9 from '@shared/components/tb-error.component'; +import * as i9$1 from '@angular/flex-layout/extended'; +import * as i12 from '@angular/material/tooltip'; +import { distinctUntilChanged, startWith, map, mergeMap, share } from 'rxjs/operators'; +import * as i7$2 from '@shared/components/tb-checkbox.component'; +import * as i5$3 from '@home/components/sms/sms-provider-configuration.component'; +import { HomeComponentsModule } from '@home/components/public-api'; +import * as i7$3 from '@shared/components/relation/relation-type-autocomplete.component'; +import * as i8$2 from '@shared/components/entity/entity-subtype-list.component'; +import * as i7$4 from '@home/components/relation/relation-filters.component'; +import { of } from 'rxjs'; +import * as i7$5 from '@angular/material/autocomplete'; +import * as i12$1 from '@shared/pipe/highlight.pipe'; +import * as i8$3 from '@shared/components/entity/entity-autocomplete.component'; +import * as i3$5 from '@shared/components/entity/entity-type-list.component'; + +class EmptyConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.emptyConfigForm; + } + onConfigurationSet(configuration) { + this.emptyConfigForm = this.fb.group({}); + } +} +EmptyConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: EmptyConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +EmptyConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: EmptyConfigComponent, selector: "tb-node-empty-config", usesInheritance: true, ngImport: i0, template: '
', isInline: true }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: EmptyConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-node-empty-config', + template: '
', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class SafeHtmlPipe { + constructor(sanitizer) { + this.sanitizer = sanitizer; + } + transform(html) { + return this.sanitizer.bypassSecurityTrustHtml(html); + } +} +SafeHtmlPipe.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SafeHtmlPipe, deps: [{ token: i1$1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); +SafeHtmlPipe.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SafeHtmlPipe, name: "safeHtml" }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SafeHtmlPipe, decorators: [{ + type: Pipe, + args: [{ + name: 'safeHtml', + }] + }], ctorParameters: function () { return [{ type: i1$1.DomSanitizer }]; } }); + +class AssignCustomerConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.assignCustomerConfigForm; + } + onConfigurationSet(configuration) { + this.assignCustomerConfigForm = this.fb.group({ + customerNamePattern: [configuration ? configuration.customerNamePattern : null, [Validators.required, Validators.pattern(/.*\S.*/)]], + createCustomerIfNotExists: [configuration ? configuration.createCustomerIfNotExists : false, []], + customerCacheExpiration: [configuration ? configuration.customerCacheExpiration : null, [Validators.required, Validators.min(0)]] + }); + } + prepareOutputConfig(configuration) { + configuration.customerNamePattern = configuration.customerNamePattern.trim(); + return configuration; + } +} +AssignCustomerConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: AssignCustomerConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +AssignCustomerConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: AssignCustomerConfigComponent, selector: "tb-action-node-assign-to-customer-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: AssignCustomerConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-assign-to-customer-config', + templateUrl: './assign-customer-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class AttributesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.attributeScopes = Object.keys(AttributeScope); + this.telemetryTypeTranslationsMap = telemetryTypeTranslations; + } + configForm() { + return this.attributesConfigForm; + } + onConfigurationSet(configuration) { + this.attributesConfigForm = this.fb.group({ + scope: [configuration ? configuration.scope : null, [Validators.required]], + notifyDevice: [configuration ? configuration.scope : true, []] + }); + } +} +AttributesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: AttributesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +AttributesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: AttributesConfigComponent, selector: "tb-action-node-attributes-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: AttributesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-attributes-config', + templateUrl: './attributes-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +var OriginatorSource; +(function (OriginatorSource) { + OriginatorSource["CUSTOMER"] = "CUSTOMER"; + OriginatorSource["TENANT"] = "TENANT"; + OriginatorSource["RELATED"] = "RELATED"; + OriginatorSource["ALARM_ORIGINATOR"] = "ALARM_ORIGINATOR"; +})(OriginatorSource || (OriginatorSource = {})); +const originatorSourceTranslations = new Map([ + [OriginatorSource.CUSTOMER, 'tb.rulenode.originator-customer'], + [OriginatorSource.TENANT, 'tb.rulenode.originator-tenant'], + [OriginatorSource.RELATED, 'tb.rulenode.originator-related'], + [OriginatorSource.ALARM_ORIGINATOR, 'tb.rulenode.originator-alarm-originator'], +]); +var PerimeterType; +(function (PerimeterType) { + PerimeterType["CIRCLE"] = "CIRCLE"; + PerimeterType["POLYGON"] = "POLYGON"; +})(PerimeterType || (PerimeterType = {})); +const perimeterTypeTranslations = new Map([ + [PerimeterType.CIRCLE, 'tb.rulenode.perimeter-circle'], + [PerimeterType.POLYGON, 'tb.rulenode.perimeter-polygon'], +]); +var TimeUnit; +(function (TimeUnit) { + TimeUnit["MILLISECONDS"] = "MILLISECONDS"; + TimeUnit["SECONDS"] = "SECONDS"; + TimeUnit["MINUTES"] = "MINUTES"; + TimeUnit["HOURS"] = "HOURS"; + TimeUnit["DAYS"] = "DAYS"; +})(TimeUnit || (TimeUnit = {})); +const timeUnitTranslations = new Map([ + [TimeUnit.MILLISECONDS, 'tb.rulenode.time-unit-milliseconds'], + [TimeUnit.SECONDS, 'tb.rulenode.time-unit-seconds'], + [TimeUnit.MINUTES, 'tb.rulenode.time-unit-minutes'], + [TimeUnit.HOURS, 'tb.rulenode.time-unit-hours'], + [TimeUnit.DAYS, 'tb.rulenode.time-unit-days'] +]); +var RangeUnit; +(function (RangeUnit) { + RangeUnit["METER"] = "METER"; + RangeUnit["KILOMETER"] = "KILOMETER"; + RangeUnit["FOOT"] = "FOOT"; + RangeUnit["MILE"] = "MILE"; + RangeUnit["NAUTICAL_MILE"] = "NAUTICAL_MILE"; +})(RangeUnit || (RangeUnit = {})); +const rangeUnitTranslations = new Map([ + [RangeUnit.METER, 'tb.rulenode.range-unit-meter'], + [RangeUnit.KILOMETER, 'tb.rulenode.range-unit-kilometer'], + [RangeUnit.FOOT, 'tb.rulenode.range-unit-foot'], + [RangeUnit.MILE, 'tb.rulenode.range-unit-mile'], + [RangeUnit.NAUTICAL_MILE, 'tb.rulenode.range-unit-nautical-mile'] +]); +var EntityDetailsField; +(function (EntityDetailsField) { + EntityDetailsField["TITLE"] = "TITLE"; + EntityDetailsField["COUNTRY"] = "COUNTRY"; + EntityDetailsField["STATE"] = "STATE"; + EntityDetailsField["CITY"] = "CITY"; + EntityDetailsField["ZIP"] = "ZIP"; + EntityDetailsField["ADDRESS"] = "ADDRESS"; + EntityDetailsField["ADDRESS2"] = "ADDRESS2"; + EntityDetailsField["PHONE"] = "PHONE"; + EntityDetailsField["EMAIL"] = "EMAIL"; + EntityDetailsField["ADDITIONAL_INFO"] = "ADDITIONAL_INFO"; +})(EntityDetailsField || (EntityDetailsField = {})); +const entityDetailsTranslations = new Map([ + [EntityDetailsField.TITLE, 'tb.rulenode.entity-details-title'], + [EntityDetailsField.COUNTRY, 'tb.rulenode.entity-details-country'], + [EntityDetailsField.STATE, 'tb.rulenode.entity-details-state'], + [EntityDetailsField.CITY, 'tb.rulenode.entity-details-city'], + [EntityDetailsField.ZIP, 'tb.rulenode.entity-details-zip'], + [EntityDetailsField.ADDRESS, 'tb.rulenode.entity-details-address'], + [EntityDetailsField.ADDRESS2, 'tb.rulenode.entity-details-address2'], + [EntityDetailsField.PHONE, 'tb.rulenode.entity-details-phone'], + [EntityDetailsField.EMAIL, 'tb.rulenode.entity-details-email'], + [EntityDetailsField.ADDITIONAL_INFO, 'tb.rulenode.entity-details-additional_info'] +]); +var FetchMode; +(function (FetchMode) { + FetchMode["FIRST"] = "FIRST"; + FetchMode["LAST"] = "LAST"; + FetchMode["ALL"] = "ALL"; +})(FetchMode || (FetchMode = {})); +var SamplingOrder; +(function (SamplingOrder) { + SamplingOrder["ASC"] = "ASC"; + SamplingOrder["DESC"] = "DESC"; +})(SamplingOrder || (SamplingOrder = {})); +var SqsQueueType; +(function (SqsQueueType) { + SqsQueueType["STANDARD"] = "STANDARD"; + SqsQueueType["FIFO"] = "FIFO"; +})(SqsQueueType || (SqsQueueType = {})); +const sqsQueueTypeTranslations = new Map([ + [SqsQueueType.STANDARD, 'tb.rulenode.sqs-queue-standard'], + [SqsQueueType.FIFO, 'tb.rulenode.sqs-queue-fifo'], +]); +const credentialsTypes = ['anonymous', 'basic', 'cert.PEM']; +const credentialsTypeTranslations = new Map([ + ['anonymous', 'tb.rulenode.credentials-anonymous'], + ['basic', 'tb.rulenode.credentials-basic'], + ['cert.PEM', 'tb.rulenode.credentials-pem'] +]); +const azureIotHubCredentialsTypes = ['sas', 'cert.PEM']; +const azureIotHubCredentialsTypeTranslations = new Map([ + ['sas', 'tb.rulenode.credentials-sas'], + ['cert.PEM', 'tb.rulenode.credentials-pem'] +]); +var HttpRequestType; +(function (HttpRequestType) { + HttpRequestType["GET"] = "GET"; + HttpRequestType["POST"] = "POST"; + HttpRequestType["PUT"] = "PUT"; + HttpRequestType["DELETE"] = "DELETE"; +})(HttpRequestType || (HttpRequestType = {})); +const ToByteStandartCharsetTypes = [ + 'US-ASCII', + 'ISO-8859-1', + 'UTF-8', + 'UTF-16BE', + 'UTF-16LE', + 'UTF-16' +]; +const ToByteStandartCharsetTypeTranslations = 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'], +]); + +class AzureIotHubConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.allAzureIotHubCredentialsTypes = azureIotHubCredentialsTypes; + this.azureIotHubCredentialsTypeTranslationsMap = azureIotHubCredentialsTypeTranslations; + } + configForm() { + return this.azureIotHubConfigForm; + } + onConfigurationSet(configuration) { + this.azureIotHubConfigForm = this.fb.group({ + topicPattern: [configuration ? configuration.topicPattern : null, [Validators.required]], + host: [configuration ? configuration.host : null, [Validators.required]], + port: [configuration ? configuration.port : null, [Validators.required, Validators.min(1), Validators.max(65535)]], + connectTimeoutSec: [configuration ? configuration.connectTimeoutSec : null, + [Validators.required, Validators.min(1), Validators.max(200)]], + clientId: [configuration ? configuration.clientId : null, [Validators.required]], + cleanSession: [configuration ? configuration.cleanSession : false, []], + ssl: [configuration ? configuration.ssl : false, []], + credentials: this.fb.group({ + type: [configuration && configuration.credentials ? configuration.credentials.type : null, [Validators.required]], + sasKey: [configuration && configuration.credentials ? configuration.credentials.sasKey : null, []], + caCert: [configuration && configuration.credentials ? configuration.credentials.caCert : null, []], + caCertFileName: [configuration && configuration.credentials ? configuration.credentials.caCertFileName : null, []], + privateKey: [configuration && configuration.credentials ? configuration.credentials.privateKey : null, []], + privateKeyFileName: [configuration && configuration.credentials ? configuration.credentials.privateKeyFileName : null, []], + cert: [configuration && configuration.credentials ? configuration.credentials.cert : null, []], + certFileName: [configuration && configuration.credentials ? configuration.credentials.certFileName : null, []], + password: [configuration && configuration.credentials ? configuration.credentials.password : null, []], + }) + }); + } + prepareOutputConfig(configuration) { + const credentialsType = configuration.credentials.type; + if (credentialsType === 'sas') { + configuration.credentials = { + type: credentialsType, + sasKey: configuration.credentials.sasKey, + caCert: configuration.credentials.caCert, + caCertFileName: configuration.credentials.caCertFileName + }; + } + return configuration; + } + validatorTriggers() { + return ['credentials.type']; + } + updateValidators(emitEvent) { + const credentialsControl = this.azureIotHubConfigForm.get('credentials'); + const credentialsType = credentialsControl.get('type').value; + if (emitEvent) { + credentialsControl.reset({ type: credentialsType }, { emitEvent: false }); + } + credentialsControl.get('sasKey').setValidators([]); + credentialsControl.get('privateKey').setValidators([]); + credentialsControl.get('privateKeyFileName').setValidators([]); + credentialsControl.get('cert').setValidators([]); + credentialsControl.get('certFileName').setValidators([]); + switch (credentialsType) { + case 'sas': + credentialsControl.get('sasKey').setValidators([Validators.required]); + break; + case 'cert.PEM': + credentialsControl.get('privateKey').setValidators([Validators.required]); + credentialsControl.get('privateKeyFileName').setValidators([Validators.required]); + credentialsControl.get('cert').setValidators([Validators.required]); + credentialsControl.get('certFileName').setValidators([Validators.required]); + break; + } + credentialsControl.get('sasKey').updateValueAndValidity({ emitEvent }); + credentialsControl.get('privateKey').updateValueAndValidity({ emitEvent }); + credentialsControl.get('privateKeyFileName').updateValueAndValidity({ emitEvent }); + credentialsControl.get('cert').updateValueAndValidity({ emitEvent }); + credentialsControl.get('certFileName').updateValueAndValidity({ emitEvent }); + } +} +AzureIotHubConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: AzureIotHubConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +AzureIotHubConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: AzureIotHubConfigComponent, selector: "tb-action-node-azure-iot-hub-config", usesInheritance: true, ngImport: i0, 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}:host .tb-hint.client-id{margin-top:-1.25em;max-width:-moz-fit-content;max-width:fit-content}\n"], components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["disabled", "expanded", "hideToggle", "togglePosition"], outputs: ["opened", "closed", "expandedChange", "afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { type: i4$2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["tabIndex", "expandedHeight", "collapsedHeight"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7.TogglePasswordComponent, selector: "tb-toggle-password" }, { type: i8$1.FileInputComponent, selector: "tb-file-input", inputs: ["label", "accept", "noFileText", "inputId", "allowedExtensions", "dropLabel", "contentConvertFunction", "required", "requiredAsError", "disabled", "existingFileName", "readAsBinary", "workFromFileObj", "multipleFile"], outputs: ["fileNameChanged"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i4$2.MatAccordion, selector: "mat-accordion", inputs: ["multi", "displayMode", "togglePosition", "hideToggle"], exportAs: ["matAccordion"] }, { type: i4$2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { type: i4$2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { type: i2.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i10.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { type: i10.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { type: i3.MatSuffix, selector: "[matSuffix]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: AzureIotHubConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-azure-iot-hub-config', + templateUrl: './azure-iot-hub-config.component.html', + styleUrls: ['./mqtt-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class CheckPointConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.serviceType = ServiceType.TB_RULE_ENGINE; + } + configForm() { + return this.checkPointConfigForm; + } + onConfigurationSet(configuration) { + this.checkPointConfigForm = this.fb.group({ + queueName: [configuration ? configuration.queueName : null, [Validators.required]] + }); + } +} +CheckPointConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckPointConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CheckPointConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CheckPointConfigComponent, selector: "tb-action-node-check-point-config", usesInheritance: true, ngImport: i0, template: "
\n \n \n
\n", components: [{ type: i3$2.QueueTypeListComponent, selector: "tb-queue-type-list", inputs: ["required", "disabled", "queueType"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckPointConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-check-point-config', + templateUrl: './check-point-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class ClearAlarmConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + } + configForm() { + return this.clearAlarmConfigForm; + } + onConfigurationSet(configuration) { + this.clearAlarmConfigForm = this.fb.group({ + alarmDetailsBuildJs: [configuration ? configuration.alarmDetailsBuildJs : null, [Validators.required]], + alarmType: [configuration ? configuration.alarmType : null, [Validators.required]] + }); + } + testScript() { + const script = this.clearAlarmConfigForm.get('alarmDetailsBuildJs').value; + this.nodeScriptTestService.testNodeScript(script, 'json', this.translate.instant('tb.rulenode.details'), 'Details', ['msg', 'metadata', 'msgType'], this.ruleNodeId, 'rulenode/clear_alarm_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.clearAlarmConfigForm.get('alarmDetailsBuildJs').setValue(theScript); + } + }); + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +ClearAlarmConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ClearAlarmConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +ClearAlarmConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: ClearAlarmConfigComponent, selector: "tb-action-node-clear-alarm-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, 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", components: [{ type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ClearAlarmConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-clear-alarm-config', + templateUrl: './clear-alarm-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class CreateAlarmConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + this.alarmSeverities = Object.keys(AlarmSeverity); + this.alarmSeverityTranslationMap = alarmSeverityTranslations; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + } + configForm() { + return this.createAlarmConfigForm; + } + onConfigurationSet(configuration) { + this.createAlarmConfigForm = this.fb.group({ + alarmDetailsBuildJs: [configuration ? configuration.alarmDetailsBuildJs : null, [Validators.required]], + useMessageAlarmData: [configuration ? configuration.useMessageAlarmData : false, []], + alarmType: [configuration ? configuration.alarmType : null, []], + severity: [configuration ? configuration.severity : null, []], + propagate: [configuration ? configuration.propagate : false, []], + relationTypes: [configuration ? configuration.relationTypes : null, []], + dynamicSeverity: false + }); + this.createAlarmConfigForm.get('dynamicSeverity').valueChanges.subscribe((dynamicSeverity) => { + if (dynamicSeverity) { + this.createAlarmConfigForm.get('severity').patchValue('', { emitEvent: false }); + } + else { + this.createAlarmConfigForm.get('severity').patchValue(this.alarmSeverities[0], { emitEvent: false }); + } + }); + } + validatorTriggers() { + return ['useMessageAlarmData']; + } + updateValidators(emitEvent) { + const useMessageAlarmData = this.createAlarmConfigForm.get('useMessageAlarmData').value; + if (useMessageAlarmData) { + this.createAlarmConfigForm.get('alarmType').setValidators([]); + this.createAlarmConfigForm.get('severity').setValidators([]); + } + else { + this.createAlarmConfigForm.get('alarmType').setValidators([Validators.required]); + this.createAlarmConfigForm.get('severity').setValidators([Validators.required]); + } + this.createAlarmConfigForm.get('alarmType').updateValueAndValidity({ emitEvent }); + this.createAlarmConfigForm.get('severity').updateValueAndValidity({ emitEvent }); + } + testScript() { + const script = this.createAlarmConfigForm.get('alarmDetailsBuildJs').value; + this.nodeScriptTestService.testNodeScript(script, 'json', this.translate.instant('tb.rulenode.details'), 'Details', ['msg', 'metadata', 'msgType'], this.ruleNodeId, 'rulenode/create_alarm_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.createAlarmConfigForm.get('alarmDetailsBuildJs').setValue(theScript); + } + }); + } + removeKey(key, keysField) { + const keys = this.createAlarmConfigForm.get(keysField).value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.createAlarmConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + addKey(event, keysField) { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let keys = this.createAlarmConfigForm.get(keysField).value; + if (!keys || keys.indexOf(value) === -1) { + if (!keys) { + keys = []; + } + keys.push(value); + this.createAlarmConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + if (input) { + input.value = ''; + } + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +CreateAlarmConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CreateAlarmConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +CreateAlarmConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CreateAlarmConfigComponent, selector: "tb-action-node-create-alarm-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, 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", components: [{ type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CreateAlarmConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-create-alarm-config', + templateUrl: './create-alarm-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class CreateRelationConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.directionTypes = Object.keys(EntitySearchDirection); + this.directionTypeTranslations = entitySearchDirectionTranslations; + this.entityType = EntityType; + } + configForm() { + return this.createRelationConfigForm; + } + onConfigurationSet(configuration) { + this.createRelationConfigForm = this.fb.group({ + direction: [configuration ? configuration.direction : null, [Validators.required]], + entityType: [configuration ? configuration.entityType : null, [Validators.required]], + entityNamePattern: [configuration ? configuration.entityNamePattern : null, []], + entityTypePattern: [configuration ? configuration.entityTypePattern : null, []], + relationType: [configuration ? configuration.relationType : null, [Validators.required]], + createEntityIfNotExists: [configuration ? configuration.createEntityIfNotExists : false, []], + removeCurrentRelations: [configuration ? configuration.removeCurrentRelations : false, []], + changeOriginatorToRelatedEntity: [configuration ? configuration.changeOriginatorToRelatedEntity : false, []], + entityCacheExpiration: [configuration ? configuration.entityCacheExpiration : null, [Validators.required, Validators.min(0)]], + }); + } + validatorTriggers() { + return ['entityType']; + } + updateValidators(emitEvent) { + const entityType = this.createRelationConfigForm.get('entityType').value; + if (entityType) { + this.createRelationConfigForm.get('entityNamePattern').setValidators([Validators.required, Validators.pattern(/.*\S.*/)]); + } + else { + this.createRelationConfigForm.get('entityNamePattern').setValidators([]); + } + if (entityType && (entityType === EntityType.DEVICE || entityType === EntityType.ASSET)) { + this.createRelationConfigForm.get('entityTypePattern').setValidators([Validators.required, Validators.pattern(/.*\S.*/)]); + } + else { + this.createRelationConfigForm.get('entityTypePattern').setValidators([]); + } + this.createRelationConfigForm.get('entityNamePattern').updateValueAndValidity({ emitEvent }); + this.createRelationConfigForm.get('entityTypePattern').updateValueAndValidity({ emitEvent }); + } + prepareOutputConfig(configuration) { + configuration.entityNamePattern = configuration.entityNamePattern ? configuration.entityNamePattern.trim() : null; + configuration.entityTypePattern = configuration.entityTypePattern ? configuration.entityTypePattern.trim() : null; + return configuration; + } +} +CreateRelationConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CreateRelationConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CreateRelationConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CreateRelationConfigComponent, selector: "tb-action-node-create-relation-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7$1.EntityTypeSelectComponent, selector: "tb-entity-type-select", inputs: ["allowedEntityTypes", "useAliasEntityTypes", "showLabel", "required", "disabled"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CreateRelationConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-create-relation-config', + templateUrl: './create-relation-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class DeleteRelationConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.directionTypes = Object.keys(EntitySearchDirection); + this.directionTypeTranslations = entitySearchDirectionTranslations; + this.entityType = EntityType; + } + configForm() { + return this.deleteRelationConfigForm; + } + onConfigurationSet(configuration) { + this.deleteRelationConfigForm = this.fb.group({ + deleteForSingleEntity: [configuration ? configuration.deleteForSingleEntity : false, []], + direction: [configuration ? configuration.direction : null, [Validators.required]], + entityType: [configuration ? configuration.entityType : null, []], + entityNamePattern: [configuration ? configuration.entityNamePattern : null, []], + relationType: [configuration ? configuration.relationType : null, [Validators.required]], + entityCacheExpiration: [configuration ? configuration.entityCacheExpiration : null, [Validators.required, Validators.min(0)]], + }); + } + validatorTriggers() { + return ['deleteForSingleEntity', 'entityType']; + } + updateValidators(emitEvent) { + const deleteForSingleEntity = this.deleteRelationConfigForm.get('deleteForSingleEntity').value; + const entityType = this.deleteRelationConfigForm.get('entityType').value; + if (deleteForSingleEntity) { + this.deleteRelationConfigForm.get('entityType').setValidators([Validators.required]); + } + else { + this.deleteRelationConfigForm.get('entityType').setValidators([]); + } + if (deleteForSingleEntity && entityType) { + this.deleteRelationConfigForm.get('entityNamePattern').setValidators([Validators.required, Validators.pattern(/.*\S.*/)]); + } + else { + this.deleteRelationConfigForm.get('entityNamePattern').setValidators([]); + } + this.deleteRelationConfigForm.get('entityType').updateValueAndValidity({ emitEvent: false }); + this.deleteRelationConfigForm.get('entityNamePattern').updateValueAndValidity({ emitEvent }); + } + prepareOutputConfig(configuration) { + configuration.entityNamePattern = configuration.entityNamePattern ? configuration.entityNamePattern.trim() : null; + return configuration; + } +} +DeleteRelationConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeleteRelationConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +DeleteRelationConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: DeleteRelationConfigComponent, selector: "tb-action-node-delete-relation-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7$1.EntityTypeSelectComponent, selector: "tb-entity-type-select", inputs: ["allowedEntityTypes", "useAliasEntityTypes", "showLabel", "required", "disabled"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeleteRelationConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-delete-relation-config', + templateUrl: './delete-relation-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class DeviceProfileConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.deviceProfile; + } + onConfigurationSet(configuration) { + this.deviceProfile = this.fb.group({ + persistAlarmRulesState: [configuration ? configuration.persistAlarmRulesState : false, Validators.required], + fetchAlarmRulesStateOnStart: [configuration ? configuration.fetchAlarmRulesStateOnStart : false, Validators.required] + }); + } +} +DeviceProfileConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeviceProfileConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +DeviceProfileConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: DeviceProfileConfigComponent, selector: "tb-device-profile-config", usesInheritance: true, ngImport: i0, template: "
\n \n {{ 'tb.rulenode.persist-alarm-rules' | translate }}\n \n \n {{ 'tb.rulenode.fetch-alarm-rules' | translate }}\n \n
\n", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeviceProfileConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-device-profile-config', + templateUrl: './device-profile-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class GeneratorConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + } + configForm() { + return this.generatorConfigForm; + } + onConfigurationSet(configuration) { + this.generatorConfigForm = this.fb.group({ + msgCount: [configuration ? configuration.msgCount : null, [Validators.required, Validators.min(0)]], + periodInSeconds: [configuration ? configuration.periodInSeconds : null, [Validators.required, Validators.min(1)]], + originator: [configuration ? configuration.originator : null, []], + jsScript: [configuration ? configuration.jsScript : null, [Validators.required]] + }); + } + prepareInputConfig(configuration) { + if (configuration) { + if (configuration.originatorId && configuration.originatorType) { + configuration.originator = { + id: configuration.originatorId, entityType: configuration.originatorType + }; + } + else { + configuration.originator = null; + } + delete configuration.originatorId; + delete configuration.originatorType; + } + return configuration; + } + prepareOutputConfig(configuration) { + if (configuration.originator) { + configuration.originatorId = configuration.originator.id; + configuration.originatorType = configuration.originator.entityType; + } + else { + configuration.originatorId = null; + configuration.originatorType = null; + } + delete configuration.originator; + return configuration; + } + testScript() { + const script = this.generatorConfigForm.get('jsScript').value; + this.nodeScriptTestService.testNodeScript(script, 'generate', this.translate.instant('tb.rulenode.generator'), 'Generate', ['prevMsg', 'prevMetadata', 'prevMsgType'], this.ruleNodeId, 'rulenode/generator_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.generatorConfigForm.get('jsScript').setValue(theScript); + } + }); + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +GeneratorConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GeneratorConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +GeneratorConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: GeneratorConfigComponent, selector: "tb-action-node-generator-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i6$2.EntitySelectComponent, selector: "tb-entity-select", inputs: ["allowedEntityTypes", "useAliasEntityTypes", "required", "disabled"] }, { type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GeneratorConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-generator-config', + templateUrl: './generator-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class GpsGeoActionConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.perimeterType = PerimeterType; + this.perimeterTypes = Object.keys(PerimeterType); + this.perimeterTypeTranslationMap = perimeterTypeTranslations; + this.rangeUnits = Object.keys(RangeUnit); + this.rangeUnitTranslationMap = rangeUnitTranslations; + this.timeUnits = Object.keys(TimeUnit); + this.timeUnitsTranslationMap = timeUnitTranslations; + } + configForm() { + return this.geoActionConfigForm; + } + onConfigurationSet(configuration) { + this.geoActionConfigForm = this.fb.group({ + latitudeKeyName: [configuration ? configuration.latitudeKeyName : null, [Validators.required]], + longitudeKeyName: [configuration ? configuration.longitudeKeyName : null, [Validators.required]], + fetchPerimeterInfoFromMessageMetadata: [configuration ? configuration.fetchPerimeterInfoFromMessageMetadata : false, []], + perimeterType: [configuration ? configuration.perimeterType : null, []], + centerLatitude: [configuration ? configuration.centerLatitude : null, []], + centerLongitude: [configuration ? configuration.centerLatitude : null, []], + range: [configuration ? configuration.range : null, []], + rangeUnit: [configuration ? configuration.rangeUnit : null, []], + polygonsDefinition: [configuration ? configuration.polygonsDefinition : null, []], + minInsideDuration: [configuration ? configuration.minInsideDuration : null, + [Validators.required, Validators.min(1), Validators.max(2147483647)]], + minInsideDurationTimeUnit: [configuration ? configuration.minInsideDurationTimeUnit : null, [Validators.required]], + minOutsideDuration: [configuration ? configuration.minOutsideDuration : null, + [Validators.required, Validators.min(1), Validators.max(2147483647)]], + minOutsideDurationTimeUnit: [configuration ? configuration.minOutsideDurationTimeUnit : null, [Validators.required]], + }); + } + validatorTriggers() { + return ['fetchPerimeterInfoFromMessageMetadata', 'perimeterType']; + } + updateValidators(emitEvent) { + const fetchPerimeterInfoFromMessageMetadata = this.geoActionConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value; + const perimeterType = this.geoActionConfigForm.get('perimeterType').value; + if (fetchPerimeterInfoFromMessageMetadata) { + this.geoActionConfigForm.get('perimeterType').setValidators([]); + } + else { + this.geoActionConfigForm.get('perimeterType').setValidators([Validators.required]); + } + if (!fetchPerimeterInfoFromMessageMetadata && perimeterType === PerimeterType.CIRCLE) { + this.geoActionConfigForm.get('centerLatitude').setValidators([Validators.required, + Validators.min(-90), Validators.max(90)]); + this.geoActionConfigForm.get('centerLongitude').setValidators([Validators.required, + Validators.min(-180), Validators.max(180)]); + this.geoActionConfigForm.get('range').setValidators([Validators.required, Validators.min(0)]); + this.geoActionConfigForm.get('rangeUnit').setValidators([Validators.required]); + } + else { + this.geoActionConfigForm.get('centerLatitude').setValidators([]); + this.geoActionConfigForm.get('centerLongitude').setValidators([]); + this.geoActionConfigForm.get('range').setValidators([]); + this.geoActionConfigForm.get('rangeUnit').setValidators([]); + } + if (!fetchPerimeterInfoFromMessageMetadata && perimeterType === PerimeterType.POLYGON) { + this.geoActionConfigForm.get('polygonsDefinition').setValidators([Validators.required]); + } + else { + this.geoActionConfigForm.get('polygonsDefinition').setValidators([]); + } + this.geoActionConfigForm.get('perimeterType').updateValueAndValidity({ emitEvent: false }); + this.geoActionConfigForm.get('centerLatitude').updateValueAndValidity({ emitEvent }); + this.geoActionConfigForm.get('centerLongitude').updateValueAndValidity({ emitEvent }); + this.geoActionConfigForm.get('range').updateValueAndValidity({ emitEvent }); + this.geoActionConfigForm.get('rangeUnit').updateValueAndValidity({ emitEvent }); + this.geoActionConfigForm.get('polygonsDefinition').updateValueAndValidity({ emitEvent }); + } +} +GpsGeoActionConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GpsGeoActionConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +GpsGeoActionConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: GpsGeoActionConfigComponent, selector: "tb-action-node-gps-geofencing-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GpsGeoActionConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-gps-geofencing-config', + templateUrl: './gps-geo-action-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class KvMapConfigComponent extends PageComponent { + constructor(store, translate, injector, fb) { + super(store); + this.store = store; + this.translate = translate; + this.injector = injector; + this.fb = fb; + this.propagateChange = null; + this.valueChangeSubscription = null; + } + get required() { + return this.requiredValue; + } + set required(value) { + this.requiredValue = coerceBooleanProperty(value); + } + ngOnInit() { + this.ngControl = this.injector.get(NgControl); + if (this.ngControl != null) { + this.ngControl.valueAccessor = this; + } + this.kvListFormGroup = this.fb.group({}); + this.kvListFormGroup.addControl('keyVals', this.fb.array([])); + } + keyValsFormArray() { + return this.kvListFormGroup.get('keyVals'); + } + registerOnChange(fn) { + this.propagateChange = fn; + } + registerOnTouched(fn) { + } + setDisabledState(isDisabled) { + this.disabled = isDisabled; + if (this.disabled) { + this.kvListFormGroup.disable({ emitEvent: false }); + } + else { + this.kvListFormGroup.enable({ emitEvent: false }); + } + } + writeValue(keyValMap) { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const keyValsControls = []; + if (keyValMap) { + for (const property of Object.keys(keyValMap)) { + if (Object.prototype.hasOwnProperty.call(keyValMap, property)) { + keyValsControls.push(this.fb.group({ + key: [property, [Validators.required]], + value: [keyValMap[property], [Validators.required]] + })); + } + } + } + this.kvListFormGroup.setControl('keyVals', this.fb.array(keyValsControls)); + this.valueChangeSubscription = this.kvListFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + removeKeyVal(index) { + this.kvListFormGroup.get('keyVals').removeAt(index); + } + addKeyVal() { + const keyValsFormArray = this.kvListFormGroup.get('keyVals'); + keyValsFormArray.push(this.fb.group({ + key: ['', [Validators.required]], + value: ['', [Validators.required]] + })); + } + validate(c) { + const kvList = this.kvListFormGroup.get('keyVals').value; + if (!kvList.length && this.required) { + return { + kvMapRequired: true + }; + } + if (!this.kvListFormGroup.valid) { + return { + kvFieldsRequired: true + }; + } + return null; + } + updateModel() { + const kvList = this.kvListFormGroup.get('keyVals').value; + if (this.required && !kvList.length || !this.kvListFormGroup.valid) { + this.propagateChange(null); + } + else { + const keyValMap = {}; + kvList.forEach((entry) => { + keyValMap[entry.key] = entry.value; + }); + this.propagateChange(keyValMap); + } + } +} +KvMapConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: KvMapConfigComponent, deps: [{ token: i1.Store }, { token: i4.TranslateService }, { token: i0.Injector }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +KvMapConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: { disabled: "disabled", requiredText: "requiredText", keyText: "keyText", keyRequiredText: "keyRequiredText", valText: "valText", valRequiredText: "valRequiredText", required: "required" }, providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KvMapConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KvMapConfigComponent), + multi: true, + } + ], usesInheritance: true, ngImport: i0, 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", 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:#0000008a;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}\n"], components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: i9.TbErrorComponent, selector: "tb-error", inputs: ["error"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i9$1.DefaultShowHideDirective, selector: " [fxShow], [fxShow.print], [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], [fxHide], [fxHide.print], [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg]", inputs: ["fxShow", "fxShow.print", "fxShow.xs", "fxShow.sm", "fxShow.md", "fxShow.lg", "fxShow.xl", "fxShow.lt-sm", "fxShow.lt-md", "fxShow.lt-lg", "fxShow.lt-xl", "fxShow.gt-xs", "fxShow.gt-sm", "fxShow.gt-md", "fxShow.gt-lg", "fxHide", "fxHide.print", "fxHide.xs", "fxHide.sm", "fxHide.md", "fxHide.lg", "fxHide.xl", "fxHide.lt-sm", "fxHide.lt-md", "fxHide.lt-lg", "fxHide.lt-xl", "fxHide.gt-xs", "fxHide.gt-sm", "fxHide.gt-md", "fxHide.gt-lg"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutAlignDirective, selector: " [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg]", inputs: ["fxLayoutAlign", "fxLayoutAlign.xs", "fxLayoutAlign.sm", "fxLayoutAlign.md", "fxLayoutAlign.lg", "fxLayoutAlign.xl", "fxLayoutAlign.lt-sm", "fxLayoutAlign.lt-md", "fxLayoutAlign.lt-lg", "fxLayoutAlign.lt-xl", "fxLayoutAlign.gt-xs", "fxLayoutAlign.gt-sm", "fxLayoutAlign.gt-md", "fxLayoutAlign.gt-lg"] }, { type: i2.FormArrayName, selector: "[formArrayName]", inputs: ["formArrayName"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlDirective, selector: "[formControl]", inputs: ["disabled", "formControl", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i12.MatTooltip, selector: "[matTooltip]", exportAs: ["matTooltip"] }], pipes: { "translate": i4.TranslatePipe, "async": i10.AsyncPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: KvMapConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-kv-map-config', + templateUrl: './kv-map-config.component.html', + styleUrls: ['./kv-map-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => KvMapConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => KvMapConfigComponent), + multi: true, + } + ] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i4.TranslateService }, { type: i0.Injector }, { type: i2.FormBuilder }]; }, propDecorators: { disabled: [{ + type: Input + }], requiredText: [{ + type: Input + }], keyText: [{ + type: Input + }], keyRequiredText: [{ + type: Input + }], valText: [{ + type: Input + }], valRequiredText: [{ + type: Input + }], required: [{ + type: Input + }] } }); + +class KafkaConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.ackValues = ['all', '-1', '0', '1']; + this.ToByteStandartCharsetTypesValues = ToByteStandartCharsetTypes; + this.ToByteStandartCharsetTypeTranslationMap = ToByteStandartCharsetTypeTranslations; + } + configForm() { + return this.kafkaConfigForm; + } + onConfigurationSet(configuration) { + this.kafkaConfigForm = this.fb.group({ + topicPattern: [configuration ? configuration.topicPattern : null, [Validators.required]], + bootstrapServers: [configuration ? configuration.bootstrapServers : null, [Validators.required]], + retries: [configuration ? configuration.retries : null, [Validators.min(0)]], + batchSize: [configuration ? configuration.batchSize : null, [Validators.min(0)]], + linger: [configuration ? configuration.linger : null, [Validators.min(0)]], + bufferMemory: [configuration ? configuration.bufferMemory : null, [Validators.min(0)]], + acks: [configuration ? configuration.acks : null, [Validators.required]], + keySerializer: [configuration ? configuration.keySerializer : null, [Validators.required]], + valueSerializer: [configuration ? configuration.valueSerializer : null, [Validators.required]], + otherProperties: [configuration ? configuration.otherProperties : null, []], + addMetadataKeyValuesAsKafkaHeaders: [configuration ? configuration.addMetadataKeyValuesAsKafkaHeaders : false, []], + kafkaHeadersCharset: [configuration ? configuration.kafkaHeadersCharset : null, []] + }); + } + validatorTriggers() { + return ['addMetadataKeyValuesAsKafkaHeaders']; + } + updateValidators(emitEvent) { + const addMetadataKeyValuesAsKafkaHeaders = this.kafkaConfigForm.get('addMetadataKeyValuesAsKafkaHeaders').value; + if (addMetadataKeyValuesAsKafkaHeaders) { + this.kafkaConfigForm.get('kafkaHeadersCharset').setValidators([Validators.required]); + } + else { + this.kafkaConfigForm.get('kafkaHeadersCharset').setValidators([]); + } + this.kafkaConfigForm.get('kafkaHeadersCharset').updateValueAndValidity({ emitEvent }); + } +} +KafkaConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: KafkaConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +KafkaConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: KafkaConfigComponent, selector: "tb-action-node-kafka-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: KafkaConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-kafka-config', + templateUrl: './kafka-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class LogConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + } + configForm() { + return this.logConfigForm; + } + onConfigurationSet(configuration) { + this.logConfigForm = this.fb.group({ + jsScript: [configuration ? configuration.jsScript : null, [Validators.required]] + }); + } + testScript() { + const script = this.logConfigForm.get('jsScript').value; + this.nodeScriptTestService.testNodeScript(script, 'string', this.translate.instant('tb.rulenode.to-string'), 'ToString', ['msg', 'metadata', 'msgType'], this.ruleNodeId, 'rulenode/log_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.logConfigForm.get('jsScript').setValue(theScript); + } + }); + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +LogConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: LogConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +LogConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: LogConfigComponent, selector: "tb-action-node-log-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "
\n \n \n \n
\n \n
\n
\n", components: [{ type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: LogConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-log-config', + templateUrl: './log-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class CredentialsConfigComponent extends PageComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.subscriptions = []; + this.disableCertPemCredentials = false; + this.passwordFieldRquired = true; + this.allCredentialsTypes = credentialsTypes; + this.credentialsTypeTranslationsMap = credentialsTypeTranslations; + this.propagateChange = null; + } + get required() { + return this.requiredValue; + } + set required(value) { + this.requiredValue = coerceBooleanProperty(value); + } + ngOnInit() { + this.credentialsConfigFormGroup = this.fb.group({ + type: [null, [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(distinctUntilChanged()).subscribe(() => { + this.updateView(); + })); + this.subscriptions.push(this.credentialsConfigFormGroup.get('type').valueChanges.subscribe(() => { + this.credentialsTypeChanged(); + })); + } + ngOnChanges(changes) { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (!change.firstChange && change.currentValue !== change.previousValue) { + if (change.currentValue && propName === 'disableCertPemCredentials') { + const credentialsTypeValue = this.credentialsConfigFormGroup.get('type').value; + if (credentialsTypeValue === 'cert.PEM') { + setTimeout(() => { + this.credentialsConfigFormGroup.get('type').patchValue('anonymous', { emitEvent: true }); + }); + } + } + } + } + } + ngOnDestroy() { + this.subscriptions.forEach(s => s.unsubscribe()); + } + writeValue(credentials) { + if (isDefinedAndNotNull(credentials)) { + this.credentialsConfigFormGroup.reset(credentials, { emitEvent: false }); + this.updateValidators(false); + } + } + setDisabledState(isDisabled) { + if (isDisabled) { + this.credentialsConfigFormGroup.disable(); + } + else { + this.credentialsConfigFormGroup.enable(); + this.updateValidators(); + } + } + updateView() { + let credentialsConfigValue = this.credentialsConfigFormGroup.value; + const credentialsTypeValue = credentialsConfigValue.type; + switch (credentialsTypeValue) { + case 'anonymous': + credentialsConfigValue = { + type: credentialsTypeValue + }; + break; + case 'basic': + credentialsConfigValue = { + type: credentialsTypeValue, + username: credentialsConfigValue.username, + password: credentialsConfigValue.password, + }; + break; + case 'cert.PEM': + delete credentialsConfigValue.username; + break; + } + this.propagateChange(credentialsConfigValue); + } + registerOnChange(fn) { + this.propagateChange = fn; + } + registerOnTouched(fn) { + } + validate(c) { + return this.credentialsConfigFormGroup.valid ? null : { + credentialsConfig: { + valid: false, + }, + }; + } + credentialsTypeChanged() { + this.credentialsConfigFormGroup.patchValue({ + username: null, + password: null, + caCert: null, + caCertFileName: null, + privateKey: null, + privateKeyFileName: null, + cert: null, + certFileName: null, + }); + this.updateValidators(); + } + updateValidators(emitEvent = false) { + const credentialsTypeValue = this.credentialsConfigFormGroup.get('type').value; + if (emitEvent) { + this.credentialsConfigFormGroup.reset({ type: credentialsTypeValue }, { emitEvent: false }); + } + this.credentialsConfigFormGroup.setValidators([]); + this.credentialsConfigFormGroup.get('username').setValidators([]); + this.credentialsConfigFormGroup.get('password').setValidators([]); + switch (credentialsTypeValue) { + case 'anonymous': + break; + case 'basic': + this.credentialsConfigFormGroup.get('username').setValidators([Validators.required]); + this.credentialsConfigFormGroup.get('password').setValidators(this.passwordFieldRquired ? [Validators.required] : []); + break; + case 'cert.PEM': + this.credentialsConfigFormGroup.setValidators([this.requiredFilesSelected(Validators.required, [['caCert', 'caCertFileName'], ['privateKey', 'privateKeyFileName', 'cert', 'certFileName']])]); + break; + } + this.credentialsConfigFormGroup.get('username').updateValueAndValidity({ emitEvent }); + this.credentialsConfigFormGroup.get('password').updateValueAndValidity({ emitEvent }); + this.credentialsConfigFormGroup.updateValueAndValidity({ emitEvent }); + } + requiredFilesSelected(validator, requiredFieldsSet = null) { + return (group) => { + if (!requiredFieldsSet) { + requiredFieldsSet = [Object.keys(group.controls)]; + } + const allRequiredFilesSelected = (group === null || group === void 0 ? void 0 : group.controls) && + requiredFieldsSet.some(arrFields => arrFields.every(k => !validator(group.controls[k]))); + return allRequiredFilesSelected ? null : { notAllRequiredFilesSelected: true }; + }; + } +} +CredentialsConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CredentialsConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CredentialsConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CredentialsConfigComponent, selector: "tb-credentials-config", inputs: { required: "required", disableCertPemCredentials: "disableCertPemCredentials", passwordFieldRquired: "passwordFieldRquired" }, providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CredentialsConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CredentialsConfigComponent), + multi: true, + } + ], usesInheritance: true, usesOnChanges: true, ngImport: i0, 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", components: [{ type: i4$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["disabled", "expanded", "hideToggle", "togglePosition"], outputs: ["opened", "closed", "expandedChange", "afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { type: i4$2.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["tabIndex", "expandedHeight", "collapsedHeight"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7.TogglePasswordComponent, selector: "tb-toggle-password" }, { type: i8$1.FileInputComponent, selector: "tb-file-input", inputs: ["label", "accept", "noFileText", "inputId", "allowedExtensions", "dropLabel", "contentConvertFunction", "required", "requiredAsError", "disabled", "existingFileName", "readAsBinary", "workFromFileObj", "multipleFile"], outputs: ["fileNameChanged"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4$2.MatExpansionPanelTitle, selector: "mat-panel-title" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i4$2.MatExpansionPanelDescription, selector: "mat-panel-description" }, { type: i4$2.MatExpansionPanelContent, selector: "ng-template[matExpansionPanelContent]" }, { type: i3.MatLabel, selector: "mat-label" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i10.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { type: i10.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i3.MatSuffix, selector: "[matSuffix]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CredentialsConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-credentials-config', + templateUrl: './credentials-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CredentialsConfigComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CredentialsConfigComponent), + multi: true, + } + ] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; }, propDecorators: { required: [{ + type: Input + }], disableCertPemCredentials: [{ + type: Input + }], passwordFieldRquired: [{ + type: Input + }] } }); + +class MqttConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.mqttConfigForm; + } + onConfigurationSet(configuration) { + this.mqttConfigForm = this.fb.group({ + topicPattern: [configuration ? configuration.topicPattern : null, [Validators.required]], + host: [configuration ? configuration.host : null, [Validators.required]], + port: [configuration ? configuration.port : null, [Validators.required, Validators.min(1), Validators.max(65535)]], + connectTimeoutSec: [configuration ? configuration.connectTimeoutSec : null, + [Validators.required, Validators.min(1), Validators.max(200)]], + clientId: [configuration ? configuration.clientId : null, []], + cleanSession: [configuration ? configuration.cleanSession : false, []], + ssl: [configuration ? configuration.ssl : false, []], + credentials: [configuration ? configuration.credentials : null, []] + }); + } +} +MqttConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MqttConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +MqttConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: MqttConfigComponent, selector: "tb-action-node-mqtt-config", usesInheritance: true, ngImport: i0, 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
tb.rulenode.client-id-hint
\n \n {{ 'tb.rulenode.clean-session' | translate }}\n \n \n {{ 'tb.rulenode.enable-ssl' | translate }}\n \n \n
\n", styles: [":host .tb-mqtt-credentials-panel-group{margin:0 6px}:host .tb-hint.client-id{margin-top:-1.25em;max-width:-moz-fit-content;max-width:fit-content}\n"], components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: CredentialsConfigComponent, selector: "tb-credentials-config", inputs: ["required", "disableCertPemCredentials", "passwordFieldRquired"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MqttConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-mqtt-config', + templateUrl: './mqtt-config.component.html', + styleUrls: ['./mqtt-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class MsgCountConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.msgCountConfigForm; + } + onConfigurationSet(configuration) { + this.msgCountConfigForm = this.fb.group({ + interval: [configuration ? configuration.interval : null, [Validators.required, Validators.min(1)]], + telemetryPrefix: [configuration ? configuration.telemetryPrefix : null, [Validators.required]] + }); + } +} +MsgCountConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MsgCountConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +MsgCountConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: MsgCountConfigComponent, selector: "tb-action-node-msg-count-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MsgCountConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-msg-count-config', + templateUrl: './msg-count-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class MsgDelayConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.msgDelayConfigForm; + } + onConfigurationSet(configuration) { + this.msgDelayConfigForm = this.fb.group({ + useMetadataPeriodInSecondsPatterns: [configuration ? configuration.useMetadataPeriodInSecondsPatterns : false, []], + periodInSeconds: [configuration ? configuration.periodInSeconds : null, []], + periodInSecondsPattern: [configuration ? configuration.periodInSecondsPattern : null, []], + maxPendingMsgs: [configuration ? configuration.maxPendingMsgs : null, + [Validators.required, Validators.min(1), Validators.max(100000)]], + }); + } + validatorTriggers() { + return ['useMetadataPeriodInSecondsPatterns']; + } + updateValidators(emitEvent) { + const useMetadataPeriodInSecondsPatterns = this.msgDelayConfigForm.get('useMetadataPeriodInSecondsPatterns').value; + if (useMetadataPeriodInSecondsPatterns) { + this.msgDelayConfigForm.get('periodInSecondsPattern').setValidators([Validators.required]); + this.msgDelayConfigForm.get('periodInSeconds').setValidators([]); + } + else { + this.msgDelayConfigForm.get('periodInSecondsPattern').setValidators([]); + this.msgDelayConfigForm.get('periodInSeconds').setValidators([Validators.required, Validators.min(0)]); + } + this.msgDelayConfigForm.get('periodInSecondsPattern').updateValueAndValidity({ emitEvent }); + this.msgDelayConfigForm.get('periodInSeconds').updateValueAndValidity({ emitEvent }); + } +} +MsgDelayConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MsgDelayConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +MsgDelayConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: MsgDelayConfigComponent, selector: "tb-action-node-msg-delay-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MsgDelayConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-msg-delay-config', + templateUrl: './msg-delay-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class PubSubConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.pubSubConfigForm; + } + onConfigurationSet(configuration) { + this.pubSubConfigForm = this.fb.group({ + projectId: [configuration ? configuration.projectId : null, [Validators.required]], + topicName: [configuration ? configuration.topicName : null, [Validators.required]], + serviceAccountKey: [configuration ? configuration.serviceAccountKey : null, [Validators.required]], + serviceAccountKeyFileName: [configuration ? configuration.serviceAccountKeyFileName : null, [Validators.required]], + messageAttributes: [configuration ? configuration.messageAttributes : null, []] + }); + } +} +PubSubConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: PubSubConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +PubSubConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: PubSubConfigComponent, selector: "tb-action-node-pub-sub-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i8$1.FileInputComponent, selector: "tb-file-input", inputs: ["label", "accept", "noFileText", "inputId", "allowedExtensions", "dropLabel", "contentConvertFunction", "required", "requiredAsError", "disabled", "existingFileName", "readAsBinary", "workFromFileObj", "multipleFile"], outputs: ["fileNameChanged"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: PubSubConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-pub-sub-config', + templateUrl: './pubsub-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class PushToCloudConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.attributeScopes = Object.keys(AttributeScope); + this.telemetryTypeTranslationsMap = telemetryTypeTranslations; + } + configForm() { + return this.pushToCloudConfigForm; + } + onConfigurationSet(configuration) { + this.pushToCloudConfigForm = this.fb.group({ + scope: [configuration ? configuration.scope : null, [Validators.required]] + }); + } +} +PushToCloudConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: PushToCloudConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +PushToCloudConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: PushToCloudConfigComponent, selector: "tb-action-node-push-to-cloud-config", usesInheritance: true, ngImport: i0, template: "
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: PushToCloudConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-push-to-cloud-config', + templateUrl: './push-to-cloud-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class PushToEdgeConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.attributeScopes = Object.keys(AttributeScope); + this.telemetryTypeTranslationsMap = telemetryTypeTranslations; + } + configForm() { + return this.pushToEdgeConfigForm; + } + onConfigurationSet(configuration) { + this.pushToEdgeConfigForm = this.fb.group({ + scope: [configuration ? configuration.scope : null, [Validators.required]] + }); + } +} +PushToEdgeConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: PushToEdgeConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +PushToEdgeConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: PushToEdgeConfigComponent, selector: "tb-action-node-push-to-edge-config", usesInheritance: true, ngImport: i0, template: "
\n \n attribute.attributes-scope\n \n \n {{ telemetryTypeTranslationsMap.get(scope) | translate }}\n \n \n \n
\n", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: PushToEdgeConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-push-to-edge-config', + templateUrl: './push-to-edge-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RabbitMqConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.messageProperties = [ + null, + 'BASIC', + 'TEXT_PLAIN', + 'MINIMAL_BASIC', + 'MINIMAL_PERSISTENT_BASIC', + 'PERSISTENT_BASIC', + 'PERSISTENT_TEXT_PLAIN' + ]; + } + configForm() { + return this.rabbitMqConfigForm; + } + onConfigurationSet(configuration) { + this.rabbitMqConfigForm = this.fb.group({ + exchangeNamePattern: [configuration ? configuration.exchangeNamePattern : null, []], + routingKeyPattern: [configuration ? configuration.routingKeyPattern : null, []], + messageProperties: [configuration ? configuration.messageProperties : null, []], + host: [configuration ? configuration.host : null, [Validators.required]], + port: [configuration ? configuration.port : null, [Validators.required, Validators.min(1), Validators.max(65535)]], + virtualHost: [configuration ? configuration.virtualHost : null, []], + username: [configuration ? configuration.username : null, []], + password: [configuration ? configuration.password : null, []], + automaticRecoveryEnabled: [configuration ? configuration.automaticRecoveryEnabled : false, []], + connectionTimeout: [configuration ? configuration.connectionTimeout : null, [Validators.min(0)]], + handshakeTimeout: [configuration ? configuration.handshakeTimeout : null, [Validators.min(0)]], + clientProperties: [configuration ? configuration.clientProperties : null, []] + }); + } +} +RabbitMqConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RabbitMqConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RabbitMqConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RabbitMqConfigComponent, selector: "tb-action-node-rabbit-mq-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7.TogglePasswordComponent, selector: "tb-toggle-password" }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i3.MatSuffix, selector: "[matSuffix]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RabbitMqConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-rabbit-mq-config', + templateUrl: './rabbit-mq-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RestApiCallConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.proxySchemes = ['http', 'https']; + this.httpRequestTypes = Object.keys(HttpRequestType); + } + configForm() { + return this.restApiCallConfigForm; + } + onConfigurationSet(configuration) { + this.restApiCallConfigForm = this.fb.group({ + restEndpointUrlPattern: [configuration ? configuration.restEndpointUrlPattern : null, [Validators.required]], + requestMethod: [configuration ? configuration.requestMethod : null, [Validators.required]], + useSimpleClientHttpFactory: [configuration ? configuration.useSimpleClientHttpFactory : false, []], + ignoreRequestBody: [configuration ? configuration.ignoreRequestBody : false, []], + enableProxy: [configuration ? configuration.enableProxy : false, []], + useSystemProxyProperties: [configuration ? configuration.enableProxy : false, []], + proxyScheme: [configuration ? configuration.proxyHost : null, []], + proxyHost: [configuration ? configuration.proxyHost : null, []], + proxyPort: [configuration ? configuration.proxyPort : null, []], + proxyUser: [configuration ? configuration.proxyUser : null, []], + proxyPassword: [configuration ? configuration.proxyPassword : null, []], + readTimeoutMs: [configuration ? configuration.readTimeoutMs : null, []], + maxParallelRequestsCount: [configuration ? configuration.maxParallelRequestsCount : null, [Validators.min(0)]], + headers: [configuration ? configuration.headers : null, []], + useRedisQueueForMsgPersistence: [configuration ? configuration.useRedisQueueForMsgPersistence : false, []], + trimQueue: [configuration ? configuration.trimQueue : false, []], + maxQueueSize: [configuration ? configuration.maxQueueSize : null, []], + credentials: [configuration ? configuration.credentials : null, []] + }); + } + validatorTriggers() { + return ['useSimpleClientHttpFactory', 'useRedisQueueForMsgPersistence', 'enableProxy', 'useSystemProxyProperties']; + } + updateValidators(emitEvent) { + const useSimpleClientHttpFactory = this.restApiCallConfigForm.get('useSimpleClientHttpFactory').value; + const useRedisQueueForMsgPersistence = this.restApiCallConfigForm.get('useRedisQueueForMsgPersistence').value; + const enableProxy = this.restApiCallConfigForm.get('enableProxy').value; + const useSystemProxyProperties = this.restApiCallConfigForm.get('useSystemProxyProperties').value; + if (enableProxy && !useSystemProxyProperties) { + this.restApiCallConfigForm.get('proxyHost').setValidators(enableProxy ? [Validators.required] : []); + this.restApiCallConfigForm.get('proxyPort').setValidators(enableProxy ? + [Validators.required, Validators.min(1), Validators.max(65535)] : []); + } + else { + this.restApiCallConfigForm.get('proxyHost').setValidators([]); + this.restApiCallConfigForm.get('proxyPort').setValidators([]); + if (useSimpleClientHttpFactory) { + this.restApiCallConfigForm.get('readTimeoutMs').setValidators([]); + } + else { + this.restApiCallConfigForm.get('readTimeoutMs').setValidators([Validators.min(0)]); + } + } + if (useRedisQueueForMsgPersistence) { + this.restApiCallConfigForm.get('maxQueueSize').setValidators([Validators.min(0)]); + } + else { + this.restApiCallConfigForm.get('maxQueueSize').setValidators([]); + } + this.restApiCallConfigForm.get('readTimeoutMs').updateValueAndValidity({ emitEvent }); + this.restApiCallConfigForm.get('maxQueueSize').updateValueAndValidity({ emitEvent }); + this.restApiCallConfigForm.get('proxyHost').updateValueAndValidity({ emitEvent }); + this.restApiCallConfigForm.get('proxyPort').updateValueAndValidity({ emitEvent }); + this.restApiCallConfigForm.get('credentials').updateValueAndValidity({ emitEvent }); + } +} +RestApiCallConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RestApiCallConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RestApiCallConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RestApiCallConfigComponent, selector: "tb-action-node-rest-api-call-config", usesInheritance: true, ngImport: i0, template: "
\n \n tb.rulenode.endpoint-url-pattern\n \n \n {{ 'tb.rulenode.endpoint-url-pattern-required' | translate }}\n \n \n \n \n tb.rulenode.request-method\n \n \n {{ requestType }}\n \n \n \n \n {{ 'tb.rulenode.enable-proxy' | translate }}\n \n \n {{ 'tb.rulenode.use-simple-client-http-factory' | translate }}\n \n \n {{ 'tb.rulenode.ignore-request-body' | translate }}\n \n
\n \n {{ 'tb.rulenode.use-system-proxy-properties' | translate }}\n \n
\n
\n \n tb.rulenode.proxy-scheme\n \n \n {{ proxyScheme }}\n \n \n \n \n tb.rulenode.proxy-host\n \n \n {{ 'tb.rulenode.proxy-host-required' | translate }}\n \n \n \n tb.rulenode.proxy-port\n \n \n {{ 'tb.rulenode.proxy-port-required' | translate }}\n \n \n {{ 'tb.rulenode.proxy-port-range' | translate }}\n \n \n
\n \n tb.rulenode.proxy-user\n \n \n \n tb.rulenode.proxy-password\n \n \n
\n
\n \n tb.rulenode.read-timeout\n \n tb.rulenode.read-timeout-hint\n \n \n tb.rulenode.max-parallel-requests-count\n \n tb.rulenode.max-parallel-requests-count-hint\n \n \n
\n \n \n \n {{ 'tb.rulenode.use-redis-queue' | translate }}\n \n
\n \n {{ 'tb.rulenode.trim-redis-queue' | translate }}\n \n \n tb.rulenode.redis-queue-max-size\n \n \n
\n \n
\n", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }, { type: CredentialsConfigComponent, selector: "tb-credentials-config", inputs: ["required", "disableCertPemCredentials", "passwordFieldRquired"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RestApiCallConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-rest-api-call-config', + templateUrl: './rest-api-call-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RpcReplyConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.rpcReplyConfigForm; + } + onConfigurationSet(configuration) { + this.rpcReplyConfigForm = this.fb.group({ + requestIdMetaDataAttribute: [configuration ? configuration.requestIdMetaDataAttribute : null, []] + }); + } +} +RpcReplyConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RpcReplyConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RpcReplyConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RpcReplyConfigComponent, selector: "tb-action-node-rpc-reply-config", usesInheritance: true, ngImport: i0, template: "
\n \n tb.rulenode.request-id-metadata-attribute\n \n \n
\n", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RpcReplyConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-rpc-reply-config', + templateUrl: './rpc-reply-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RpcRequestConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.rpcRequestConfigForm; + } + onConfigurationSet(configuration) { + this.rpcRequestConfigForm = this.fb.group({ + timeoutInSeconds: [configuration ? configuration.timeoutInSeconds : null, [Validators.required, Validators.min(0)]] + }); + } +} +RpcRequestConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RpcRequestConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RpcRequestConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RpcRequestConfigComponent, selector: "tb-action-node-rpc-request-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RpcRequestConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-rpc-request-config', + templateUrl: './rpc-request-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class SaveToCustomTableConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.saveToCustomTableConfigForm; + } + onConfigurationSet(configuration) { + this.saveToCustomTableConfigForm = this.fb.group({ + tableName: [configuration ? configuration.tableName : null, [Validators.required, Validators.pattern(/.*\S.*/)]], + fieldsMapping: [configuration ? configuration.fieldsMapping : null, [Validators.required]] + }); + } + prepareOutputConfig(configuration) { + configuration.tableName = configuration.tableName.trim(); + return configuration; + } +} +SaveToCustomTableConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SaveToCustomTableConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +SaveToCustomTableConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: SaveToCustomTableConfigComponent, selector: "tb-action-node-custom-table-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SaveToCustomTableConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-custom-table-config', + templateUrl: './save-to-custom-table-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class SendEmailConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.smtpProtocols = [ + 'smtp', + 'smtps' + ]; + this.tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; + } + configForm() { + return this.sendEmailConfigForm; + } + onConfigurationSet(configuration) { + this.sendEmailConfigForm = this.fb.group({ + useSystemSmtpSettings: [configuration ? configuration.useSystemSmtpSettings : false, []], + smtpProtocol: [configuration ? configuration.smtpProtocol : null, []], + smtpHost: [configuration ? configuration.smtpHost : null, []], + smtpPort: [configuration ? configuration.smtpPort : null, []], + timeout: [configuration ? configuration.timeout : null, []], + enableTls: [configuration ? configuration.enableTls : false, []], + tlsVersion: [configuration ? configuration.tlsVersion : null, []], + enableProxy: [configuration ? configuration.enableProxy : false, []], + proxyHost: [configuration ? configuration.proxyHost : null, []], + proxyPort: [configuration ? configuration.proxyPort : null, []], + proxyUser: [configuration ? configuration.proxyUser : null, []], + proxyPassword: [configuration ? configuration.proxyPassword : null, []], + username: [configuration ? configuration.username : null, []], + password: [configuration ? configuration.password : null, []] + }); + } + validatorTriggers() { + return ['useSystemSmtpSettings', 'enableProxy']; + } + updateValidators(emitEvent) { + const useSystemSmtpSettings = this.sendEmailConfigForm.get('useSystemSmtpSettings').value; + const enableProxy = this.sendEmailConfigForm.get('enableProxy').value; + if (useSystemSmtpSettings) { + 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([]); + } + else { + this.sendEmailConfigForm.get('smtpProtocol').setValidators([Validators.required]); + this.sendEmailConfigForm.get('smtpHost').setValidators([Validators.required]); + this.sendEmailConfigForm.get('smtpPort').setValidators([Validators.required, Validators.min(1), Validators.max(65535)]); + this.sendEmailConfigForm.get('timeout').setValidators([Validators.required, Validators.min(0)]); + this.sendEmailConfigForm.get('proxyHost').setValidators(enableProxy ? [Validators.required] : []); + this.sendEmailConfigForm.get('proxyPort').setValidators(enableProxy ? + [Validators.required, Validators.min(1), Validators.max(65535)] : []); + } + this.sendEmailConfigForm.get('smtpProtocol').updateValueAndValidity({ emitEvent }); + this.sendEmailConfigForm.get('smtpHost').updateValueAndValidity({ emitEvent }); + this.sendEmailConfigForm.get('smtpPort').updateValueAndValidity({ emitEvent }); + this.sendEmailConfigForm.get('timeout').updateValueAndValidity({ emitEvent }); + this.sendEmailConfigForm.get('proxyHost').updateValueAndValidity({ emitEvent }); + this.sendEmailConfigForm.get('proxyPort').updateValueAndValidity({ emitEvent }); + } +} +SendEmailConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SendEmailConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +SendEmailConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: SendEmailConfigComponent, selector: "tb-action-node-send-email-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7$2.TbCheckboxComponent, selector: "tb-checkbox", inputs: ["disabled", "trueValue", "falseValue"], outputs: ["valueChange"] }, { type: i7.TogglePasswordComponent, selector: "tb-toggle-password" }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i3.MatSuffix, selector: "[matSuffix]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SendEmailConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-send-email-config', + templateUrl: './send-email-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class SendSmsConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.sendSmsConfigForm; + } + onConfigurationSet(configuration) { + this.sendSmsConfigForm = this.fb.group({ + numbersToTemplate: [configuration ? configuration.numbersToTemplate : null, [Validators.required]], + smsMessageTemplate: [configuration ? configuration.smsMessageTemplate : null, [Validators.required]], + useSystemSmsSettings: [configuration ? configuration.useSystemSmsSettings : false, []], + smsProviderConfiguration: [configuration ? configuration.smsProviderConfiguration : null, []], + }); + } + validatorTriggers() { + return ['useSystemSmsSettings']; + } + updateValidators(emitEvent) { + const useSystemSmsSettings = this.sendSmsConfigForm.get('useSystemSmsSettings').value; + if (useSystemSmsSettings) { + this.sendSmsConfigForm.get('smsProviderConfiguration').setValidators([]); + } + else { + this.sendSmsConfigForm.get('smsProviderConfiguration').setValidators([Validators.required]); + } + this.sendSmsConfigForm.get('smsProviderConfiguration').updateValueAndValidity({ emitEvent }); + } +} +SendSmsConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SendSmsConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +SendSmsConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: SendSmsConfigComponent, selector: "tb-action-node-send-sms-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i5$3.SmsProviderConfigurationComponent, selector: "tb-sms-provider-configuration", inputs: ["required", "disabled"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SendSmsConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-send-sms-config', + templateUrl: './send-sms-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class SnsConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.snsConfigForm; + } + onConfigurationSet(configuration) { + this.snsConfigForm = this.fb.group({ + topicArnPattern: [configuration ? configuration.topicArnPattern : null, [Validators.required]], + accessKeyId: [configuration ? configuration.accessKeyId : null, [Validators.required]], + secretAccessKey: [configuration ? configuration.secretAccessKey : null, [Validators.required]], + region: [configuration ? configuration.region : null, [Validators.required]] + }); + } +} +SnsConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SnsConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +SnsConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: SnsConfigComponent, selector: "tb-action-node-sns-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SnsConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-sns-config', + templateUrl: './sns-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class SqsConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.sqsQueueType = SqsQueueType; + this.sqsQueueTypes = Object.keys(SqsQueueType); + this.sqsQueueTypeTranslationsMap = sqsQueueTypeTranslations; + } + configForm() { + return this.sqsConfigForm; + } + onConfigurationSet(configuration) { + this.sqsConfigForm = this.fb.group({ + queueType: [configuration ? configuration.queueType : null, [Validators.required]], + queueUrlPattern: [configuration ? configuration.queueUrlPattern : null, [Validators.required]], + delaySeconds: [configuration ? configuration.delaySeconds : null, [Validators.min(0), Validators.max(900)]], + messageAttributes: [configuration ? configuration.messageAttributes : null, []], + accessKeyId: [configuration ? configuration.accessKeyId : null, [Validators.required]], + secretAccessKey: [configuration ? configuration.secretAccessKey : null, [Validators.required]], + region: [configuration ? configuration.region : null, [Validators.required]] + }); + } +} +SqsConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SqsConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +SqsConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: SqsConfigComponent, selector: "tb-action-node-sqs-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SqsConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-sqs-config', + templateUrl: './sqs-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class TimeseriesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.timeseriesConfigForm; + } + onConfigurationSet(configuration) { + this.timeseriesConfigForm = this.fb.group({ + defaultTTL: [configuration ? configuration.defaultTTL : null, [Validators.required, Validators.min(0)]] + }); + } +} +TimeseriesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: TimeseriesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +TimeseriesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: TimeseriesConfigComponent, selector: "tb-action-node-timeseries-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: TimeseriesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-timeseries-config', + templateUrl: './timeseries-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class UnassignCustomerConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.unassignCustomerConfigForm; + } + onConfigurationSet(configuration) { + this.unassignCustomerConfigForm = this.fb.group({ + customerNamePattern: [configuration ? configuration.customerNamePattern : null, [Validators.required, Validators.pattern(/.*\S.*/)]], + customerCacheExpiration: [configuration ? configuration.customerCacheExpiration : null, [Validators.required, Validators.min(0)]] + }); + } + prepareOutputConfig(configuration) { + configuration.customerNamePattern = configuration.customerNamePattern.trim(); + return configuration; + } +} +UnassignCustomerConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: UnassignCustomerConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +UnassignCustomerConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: UnassignCustomerConfigComponent, selector: "tb-action-node-un-assign-to-customer-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: UnassignCustomerConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-action-node-un-assign-to-customer-config', + templateUrl: './unassign-customer-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class DeviceRelationsQueryConfigComponent extends PageComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.directionTypes = Object.keys(EntitySearchDirection); + this.directionTypeTranslations = entitySearchDirectionTranslations; + this.entityType = EntityType; + this.propagateChange = null; + } + get required() { + return this.requiredValue; + } + set required(value) { + this.requiredValue = coerceBooleanProperty(value); + } + ngOnInit() { + this.deviceRelationsQueryFormGroup = this.fb.group({ + fetchLastLevelOnly: [false, []], + direction: [null, [Validators.required]], + maxLevel: [null, []], + relationType: [null], + deviceTypes: [null, [Validators.required]] + }); + this.deviceRelationsQueryFormGroup.valueChanges.subscribe((query) => { + if (this.deviceRelationsQueryFormGroup.valid) { + this.propagateChange(query); + } + else { + this.propagateChange(null); + } + }); + } + registerOnChange(fn) { + this.propagateChange = fn; + } + registerOnTouched(fn) { + } + setDisabledState(isDisabled) { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceRelationsQueryFormGroup.disable({ emitEvent: false }); + } + else { + this.deviceRelationsQueryFormGroup.enable({ emitEvent: false }); + } + } + writeValue(query) { + this.deviceRelationsQueryFormGroup.reset(query, { emitEvent: false }); + } +} +DeviceRelationsQueryConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeviceRelationsQueryConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +DeviceRelationsQueryConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: DeviceRelationsQueryConfigComponent, selector: "tb-device-relations-query-config", inputs: { disabled: "disabled", required: "required" }, providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceRelationsQueryConfigComponent), + multi: true + } + ], usesInheritance: true, ngImport: i0, 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", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7$3.RelationTypeAutocompleteComponent, selector: "tb-relation-type-autocomplete", inputs: ["required", "disabled"] }, { type: i8$2.EntitySubTypeListComponent, selector: "tb-entity-subtype-list", inputs: ["required", "disabled", "entityType"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeviceRelationsQueryConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-device-relations-query-config', + templateUrl: './device-relations-query-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceRelationsQueryConfigComponent), + multi: true + } + ] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; }, propDecorators: { disabled: [{ + type: Input + }], required: [{ + type: Input + }] } }); + +class RelationsQueryConfigComponent extends PageComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.directionTypes = Object.keys(EntitySearchDirection); + this.directionTypeTranslations = entitySearchDirectionTranslations; + this.propagateChange = null; + } + get required() { + return this.requiredValue; + } + set required(value) { + this.requiredValue = coerceBooleanProperty(value); + } + ngOnInit() { + this.relationsQueryFormGroup = this.fb.group({ + fetchLastLevelOnly: [false, []], + direction: [null, [Validators.required]], + maxLevel: [null, []], + filters: [null] + }); + this.relationsQueryFormGroup.valueChanges.subscribe((query) => { + if (this.relationsQueryFormGroup.valid) { + this.propagateChange(query); + } + else { + this.propagateChange(null); + } + }); + } + registerOnChange(fn) { + this.propagateChange = fn; + } + registerOnTouched(fn) { + } + setDisabledState(isDisabled) { + this.disabled = isDisabled; + if (this.disabled) { + this.relationsQueryFormGroup.disable({ emitEvent: false }); + } + else { + this.relationsQueryFormGroup.enable({ emitEvent: false }); + } + } + writeValue(query) { + this.relationsQueryFormGroup.reset(query || {}, { emitEvent: false }); + } +} +RelationsQueryConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RelationsQueryConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RelationsQueryConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RelationsQueryConfigComponent, selector: "tb-relations-query-config", inputs: { disabled: "disabled", required: "required" }, providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationsQueryConfigComponent), + multi: true + } + ], usesInheritance: true, ngImport: i0, 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", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7$4.RelationFiltersComponent, selector: "tb-relation-filters", inputs: ["disabled", "allowedEntityTypes"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RelationsQueryConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-relations-query-config', + templateUrl: './relations-query-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => RelationsQueryConfigComponent), + multi: true + } + ] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; }, propDecorators: { disabled: [{ + type: Input + }], required: [{ + type: Input + }] } }); + +class MessageTypesConfigComponent extends PageComponent { + constructor(store, translate, truncate, fb) { + super(store); + this.store = store; + this.translate = translate; + this.truncate = truncate; + this.fb = fb; + this.placeholder = 'tb.rulenode.message-type'; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + this.messageTypes = []; + this.messageTypesList = []; + this.searchText = ''; + this.propagateChange = (v) => { }; + this.messageTypeConfigForm = this.fb.group({ + messageType: [null] + }); + for (const type of Object.keys(MessageType)) { + this.messageTypesList.push({ + name: messageTypeNames.get(MessageType[type]), + value: type + }); + } + } + get required() { + return this.requiredValue; + } + set required(value) { + this.requiredValue = coerceBooleanProperty(value); + } + registerOnChange(fn) { + this.propagateChange = fn; + } + registerOnTouched(fn) { + } + ngOnInit() { + this.filteredMessageTypes = this.messageTypeConfigForm.get('messageType').valueChanges + .pipe(startWith(''), map((value) => value ? value : ''), mergeMap(name => this.fetchMessageTypes(name)), share()); + } + ngAfterViewInit() { } + setDisabledState(isDisabled) { + this.disabled = isDisabled; + if (this.disabled) { + this.messageTypeConfigForm.disable({ emitEvent: false }); + } + else { + this.messageTypeConfigForm.enable({ emitEvent: false }); + } + } + writeValue(value) { + this.searchText = ''; + this.messageTypes.length = 0; + if (value) { + value.forEach((type) => { + const found = this.messageTypesList.find((messageType => messageType.value === type)); + if (found) { + this.messageTypes.push({ + name: found.name, + value: found.value + }); + } + else { + this.messageTypes.push({ + name: type, + value: type + }); + } + }); + } + } + displayMessageTypeFn(messageType) { + return messageType ? messageType.name : undefined; + } + textIsNotEmpty(text) { + return (text && text != null && text.length > 0) ? true : false; + } + createMessageType($event, value) { + $event.preventDefault(); + this.transformMessageType(value); + } + add(event) { + this.transformMessageType(event.value); + } + fetchMessageTypes(searchText) { + this.searchText = searchText; + if (this.searchText && this.searchText.length) { + const search = this.searchText.toUpperCase(); + return of(this.messageTypesList.filter(messageType => messageType.name.toUpperCase().includes(search))); + } + else { + return of(this.messageTypesList); + } + } + transformMessageType(value) { + if ((value || '').trim()) { + let newMessageType = null; + const messageTypeName = value.trim(); + const existingMessageType = this.messageTypesList.find(messageType => messageType.name === messageTypeName); + if (existingMessageType) { + newMessageType = { + name: existingMessageType.name, + value: existingMessageType.value + }; + } + else { + newMessageType = { + name: messageTypeName, + value: messageTypeName + }; + } + if (newMessageType) { + this.addMessageType(newMessageType); + } + } + this.clear(''); + } + remove(messageType) { + const index = this.messageTypes.indexOf(messageType); + if (index >= 0) { + this.messageTypes.splice(index, 1); + this.updateModel(); + } + } + selected(event) { + this.addMessageType(event.option.value); + this.clear(''); + } + addMessageType(messageType) { + const index = this.messageTypes.findIndex(existingMessageType => existingMessageType.value === messageType.value); + if (index === -1) { + this.messageTypes.push(messageType); + this.updateModel(); + } + } + onFocus() { + this.messageTypeConfigForm.get('messageType').updateValueAndValidity({ onlySelf: true, emitEvent: true }); + } + clear(value = '') { + this.messageTypeInput.nativeElement.value = value; + this.messageTypeConfigForm.get('messageType').patchValue(null, { emitEvent: true }); + setTimeout(() => { + this.messageTypeInput.nativeElement.blur(); + this.messageTypeInput.nativeElement.focus(); + }, 0); + } + updateModel() { + const value = this.messageTypes.map((messageType => messageType.value)); + if (this.required) { + this.chipList.errorState = !value.length; + this.propagateChange(value.length > 0 ? value : null); + } + else { + this.chipList.errorState = false; + this.propagateChange(value); + } + } +} +MessageTypesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MessageTypesConfigComponent, deps: [{ token: i1.Store }, { token: i4.TranslateService }, { token: i3$4.TruncatePipe }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +MessageTypesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: MessageTypesConfigComponent, selector: "tb-message-types-config", inputs: { required: "required", label: "label", placeholder: "placeholder", disabled: "disabled" }, providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MessageTypesConfigComponent), + multi: true + } + ], viewQueries: [{ propertyName: "chipList", first: true, predicate: ["chipList"], descendants: true }, { propertyName: "matAutocomplete", first: true, predicate: ["messageTypeAutocomplete"], descendants: true }, { propertyName: "messageTypeInput", first: true, predicate: ["messageTypeInput"], descendants: true }], usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: i7$5.MatAutocomplete, selector: "mat-autocomplete", inputs: ["disableRipple"], exportAs: ["matAutocomplete"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }], directives: [{ type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i7$5.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", exportAs: ["matAutocompleteTrigger"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i7$5.MatAutocompleteOrigin, selector: "[matAutocompleteOrigin]", exportAs: ["matAutocompleteOrigin"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe, "async": i10.AsyncPipe, "highlight": i12$1.HighlightPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MessageTypesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-message-types-config', + templateUrl: './message-types-config.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MessageTypesConfigComponent), + multi: true + } + ] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i4.TranslateService }, { type: i3$4.TruncatePipe }, { type: i2.FormBuilder }]; }, propDecorators: { required: [{ + type: Input + }], label: [{ + type: Input + }], placeholder: [{ + type: Input + }], disabled: [{ + type: Input + }], chipList: [{ + type: ViewChild, + args: ['chipList', { static: false }] + }], matAutocomplete: [{ + type: ViewChild, + args: ['messageTypeAutocomplete', { static: false }] + }], messageTypeInput: [{ + type: ViewChild, + args: ['messageTypeInput', { static: false }] + }] } }); + +class RulenodeCoreConfigCommonModule { +} +RulenodeCoreConfigCommonModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigCommonModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +RulenodeCoreConfigCommonModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigCommonModule, declarations: [KvMapConfigComponent, + DeviceRelationsQueryConfigComponent, + RelationsQueryConfigComponent, + MessageTypesConfigComponent, + CredentialsConfigComponent, + SafeHtmlPipe], imports: [CommonModule, + SharedModule, + HomeComponentsModule], exports: [KvMapConfigComponent, + DeviceRelationsQueryConfigComponent, + RelationsQueryConfigComponent, + MessageTypesConfigComponent, + CredentialsConfigComponent, + SafeHtmlPipe] }); +RulenodeCoreConfigCommonModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigCommonModule, imports: [[ + CommonModule, + SharedModule, + HomeComponentsModule + ]] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigCommonModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + KvMapConfigComponent, + DeviceRelationsQueryConfigComponent, + RelationsQueryConfigComponent, + MessageTypesConfigComponent, + CredentialsConfigComponent, + SafeHtmlPipe + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule + ], + exports: [ + KvMapConfigComponent, + DeviceRelationsQueryConfigComponent, + RelationsQueryConfigComponent, + MessageTypesConfigComponent, + CredentialsConfigComponent, + SafeHtmlPipe + ] + }] + }] }); + +class RuleNodeCoreConfigActionModule { +} +RuleNodeCoreConfigActionModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigActionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +RuleNodeCoreConfigActionModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigActionModule, declarations: [AttributesConfigComponent, + TimeseriesConfigComponent, + RpcRequestConfigComponent, + LogConfigComponent, + AssignCustomerConfigComponent, + ClearAlarmConfigComponent, + CreateAlarmConfigComponent, + CreateRelationConfigComponent, + MsgDelayConfigComponent, + DeleteRelationConfigComponent, + GeneratorConfigComponent, + GpsGeoActionConfigComponent, + MsgCountConfigComponent, + RpcReplyConfigComponent, + SaveToCustomTableConfigComponent, + UnassignCustomerConfigComponent, + SnsConfigComponent, + SqsConfigComponent, + PubSubConfigComponent, + KafkaConfigComponent, + MqttConfigComponent, + RabbitMqConfigComponent, + RestApiCallConfigComponent, + SendEmailConfigComponent, + CheckPointConfigComponent, + AzureIotHubConfigComponent, + DeviceProfileConfigComponent, + SendSmsConfigComponent, + PushToEdgeConfigComponent, + PushToCloudConfigComponent], imports: [CommonModule, + SharedModule, + HomeComponentsModule, + RulenodeCoreConfigCommonModule], exports: [AttributesConfigComponent, + TimeseriesConfigComponent, + RpcRequestConfigComponent, + LogConfigComponent, + AssignCustomerConfigComponent, + ClearAlarmConfigComponent, + CreateAlarmConfigComponent, + CreateRelationConfigComponent, + MsgDelayConfigComponent, + DeleteRelationConfigComponent, + GeneratorConfigComponent, + GpsGeoActionConfigComponent, + MsgCountConfigComponent, + RpcReplyConfigComponent, + SaveToCustomTableConfigComponent, + UnassignCustomerConfigComponent, + SnsConfigComponent, + SqsConfigComponent, + PubSubConfigComponent, + KafkaConfigComponent, + MqttConfigComponent, + RabbitMqConfigComponent, + RestApiCallConfigComponent, + SendEmailConfigComponent, + CheckPointConfigComponent, + AzureIotHubConfigComponent, + DeviceProfileConfigComponent, + SendSmsConfigComponent, + PushToEdgeConfigComponent, + PushToCloudConfigComponent] }); +RuleNodeCoreConfigActionModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigActionModule, imports: [[ + CommonModule, + SharedModule, + HomeComponentsModule, + RulenodeCoreConfigCommonModule + ]] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigActionModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + AttributesConfigComponent, + TimeseriesConfigComponent, + RpcRequestConfigComponent, + LogConfigComponent, + AssignCustomerConfigComponent, + ClearAlarmConfigComponent, + CreateAlarmConfigComponent, + CreateRelationConfigComponent, + MsgDelayConfigComponent, + DeleteRelationConfigComponent, + GeneratorConfigComponent, + GpsGeoActionConfigComponent, + MsgCountConfigComponent, + RpcReplyConfigComponent, + SaveToCustomTableConfigComponent, + UnassignCustomerConfigComponent, + SnsConfigComponent, + SqsConfigComponent, + PubSubConfigComponent, + KafkaConfigComponent, + MqttConfigComponent, + RabbitMqConfigComponent, + RestApiCallConfigComponent, + SendEmailConfigComponent, + CheckPointConfigComponent, + AzureIotHubConfigComponent, + DeviceProfileConfigComponent, + SendSmsConfigComponent, + PushToEdgeConfigComponent, + PushToCloudConfigComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + RulenodeCoreConfigCommonModule + ], + exports: [ + AttributesConfigComponent, + TimeseriesConfigComponent, + RpcRequestConfigComponent, + LogConfigComponent, + AssignCustomerConfigComponent, + ClearAlarmConfigComponent, + CreateAlarmConfigComponent, + CreateRelationConfigComponent, + MsgDelayConfigComponent, + DeleteRelationConfigComponent, + GeneratorConfigComponent, + GpsGeoActionConfigComponent, + MsgCountConfigComponent, + RpcReplyConfigComponent, + SaveToCustomTableConfigComponent, + UnassignCustomerConfigComponent, + SnsConfigComponent, + SqsConfigComponent, + PubSubConfigComponent, + KafkaConfigComponent, + MqttConfigComponent, + RabbitMqConfigComponent, + RestApiCallConfigComponent, + SendEmailConfigComponent, + CheckPointConfigComponent, + AzureIotHubConfigComponent, + DeviceProfileConfigComponent, + SendSmsConfigComponent, + PushToEdgeConfigComponent, + PushToCloudConfigComponent + ] + }] + }] }); + +class CalculateDeltaConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + } + configForm() { + return this.calculateDeltaConfigForm; + } + onConfigurationSet(configuration) { + this.calculateDeltaConfigForm = this.fb.group({ + inputValueKey: [configuration ? configuration.inputValueKey : null, [Validators.required]], + outputValueKey: [configuration ? configuration.outputValueKey : null, [Validators.required]], + useCache: [configuration ? configuration.useCache : null, []], + addPeriodBetweenMsgs: [configuration ? configuration.addPeriodBetweenMsgs : false, []], + periodValueKey: [configuration ? configuration.periodValueKey : null, []], + round: [configuration ? configuration.round : null, [Validators.min(0), Validators.max(15)]], + tellFailureIfDeltaIsNegative: [configuration ? configuration.tellFailureIfDeltaIsNegative : null, []] + }); + } + updateValidators(emitEvent) { + const addPeriodBetweenMsgs = this.calculateDeltaConfigForm.get('addPeriodBetweenMsgs').value; + if (addPeriodBetweenMsgs) { + this.calculateDeltaConfigForm.get('periodValueKey').setValidators([Validators.required]); + } + else { + this.calculateDeltaConfigForm.get('periodValueKey').setValidators([]); + } + this.calculateDeltaConfigForm.get('periodValueKey').updateValueAndValidity({ emitEvent }); + } + validatorTriggers() { + return ['addPeriodBetweenMsgs']; + } +} +CalculateDeltaConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CalculateDeltaConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CalculateDeltaConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CalculateDeltaConfigComponent, selector: "tb-enrichment-node-calculate-delta-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CalculateDeltaConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-calculate-delta-config', + templateUrl: './calculate-delta-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class CustomerAttributesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.customerAttributesConfigForm; + } + onConfigurationSet(configuration) { + this.customerAttributesConfigForm = this.fb.group({ + telemetry: [configuration ? configuration.telemetry : false, []], + attrMapping: [configuration ? configuration.attrMapping : null, [Validators.required]] + }); + } +} +CustomerAttributesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CustomerAttributesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CustomerAttributesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CustomerAttributesConfigComponent, selector: "tb-enrichment-node-customer-attributes-config", usesInheritance: true, ngImport: i0, template: "
\n \n \n {{ 'tb.rulenode.latest-telemetry' | translate }}\n \n \n \n
\n", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CustomerAttributesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-customer-attributes-config', + templateUrl: './customer-attributes-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class DeviceAttributesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + } + configForm() { + return this.deviceAttributesConfigForm; + } + onConfigurationSet(configuration) { + this.deviceAttributesConfigForm = this.fb.group({ + deviceRelationsQuery: [configuration ? configuration.deviceRelationsQuery : null, [Validators.required]], + tellFailureIfAbsent: [configuration ? configuration.tellFailureIfAbsent : false, []], + clientAttributeNames: [configuration ? configuration.clientAttributeNames : null, []], + sharedAttributeNames: [configuration ? configuration.sharedAttributeNames : null, []], + serverAttributeNames: [configuration ? configuration.serverAttributeNames : null, []], + latestTsKeyNames: [configuration ? configuration.latestTsKeyNames : null, []], + getLatestValueWithTs: [configuration ? configuration.getLatestValueWithTs : false, []] + }); + } + removeKey(key, keysField) { + const keys = this.deviceAttributesConfigForm.get(keysField).value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.deviceAttributesConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + addKey(event, keysField) { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let keys = this.deviceAttributesConfigForm.get(keysField).value; + if (!keys || keys.indexOf(value) === -1) { + if (!keys) { + keys = []; + } + keys.push(value); + this.deviceAttributesConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + if (input) { + input.value = ''; + } + } +} +DeviceAttributesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeviceAttributesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +DeviceAttributesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: DeviceAttributesConfigComponent, selector: "tb-enrichment-node-device-attributes-config", usesInheritance: true, ngImport: i0, 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}\n"], components: [{ type: DeviceRelationsQueryConfigComponent, selector: "tb-device-relations-query-config", inputs: ["disabled", "required"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: DeviceAttributesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-device-attributes-config', + templateUrl: './device-attributes-config.component.html', + styleUrls: ['./device-attributes-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class EntityDetailsConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, translate, fb) { + super(store); + this.store = store; + this.translate = translate; + this.fb = fb; + this.entityDetailsTranslationsMap = entityDetailsTranslations; + this.entityDetailsList = []; + this.searchText = ''; + this.displayDetailsFn = this.displayDetails.bind(this); + for (const field of Object.keys(EntityDetailsField)) { + this.entityDetailsList.push(EntityDetailsField[field]); + } + this.detailsFormControl = new FormControl(''); + this.filteredEntityDetails = this.detailsFormControl.valueChanges + .pipe(startWith(''), map((value) => value ? value : ''), mergeMap(name => this.fetchEntityDetails(name)), share()); + } + ngOnInit() { + super.ngOnInit(); + } + configForm() { + return this.entityDetailsConfigForm; + } + prepareInputConfig(configuration) { + this.searchText = ''; + this.detailsFormControl.patchValue('', { emitEvent: true }); + return configuration; + } + onConfigurationSet(configuration) { + this.entityDetailsConfigForm = this.fb.group({ + detailsList: [configuration ? configuration.detailsList : null, [Validators.required]], + addToMetadata: [configuration ? configuration.addToMetadata : false, []] + }); + } + displayDetails(details) { + return details ? this.translate.instant(entityDetailsTranslations.get(details)) : undefined; + } + fetchEntityDetails(searchText) { + this.searchText = searchText; + if (this.searchText && this.searchText.length) { + const search = this.searchText.toUpperCase(); + return of(this.entityDetailsList.filter(field => this.translate.instant(entityDetailsTranslations.get(EntityDetailsField[field])).toUpperCase().includes(search))); + } + else { + return of(this.entityDetailsList); + } + } + detailsFieldSelected(event) { + this.addDetailsField(event.option.value); + this.clear(''); + } + removeDetailsField(details) { + const detailsList = this.entityDetailsConfigForm.get('detailsList').value; + if (detailsList) { + const index = detailsList.indexOf(details); + if (index >= 0) { + detailsList.splice(index, 1); + this.entityDetailsConfigForm.get('detailsList').setValue(detailsList); + } + } + } + addDetailsField(details) { + let detailsList = this.entityDetailsConfigForm.get('detailsList').value; + if (!detailsList) { + detailsList = []; + } + const index = detailsList.indexOf(details); + if (index === -1) { + detailsList.push(details); + this.entityDetailsConfigForm.get('detailsList').setValue(detailsList); + } + } + onEntityDetailsInputFocus() { + this.detailsFormControl.updateValueAndValidity({ onlySelf: true, emitEvent: true }); + } + clear(value = '') { + this.detailsInput.nativeElement.value = value; + this.detailsFormControl.patchValue(null, { emitEvent: true }); + setTimeout(() => { + this.detailsInput.nativeElement.blur(); + this.detailsInput.nativeElement.focus(); + }, 0); + } +} +EntityDetailsConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: EntityDetailsConfigComponent, deps: [{ token: i1.Store }, { token: i4.TranslateService }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +EntityDetailsConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: EntityDetailsConfigComponent, selector: "tb-enrichment-node-entity-details-config", viewQueries: [{ propertyName: "detailsInput", first: true, predicate: ["detailsInput"], descendants: true }], usesInheritance: true, ngImport: i0, 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}\n"], components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: i7$5.MatAutocomplete, selector: "mat-autocomplete", inputs: ["disableRipple"], exportAs: ["matAutocomplete"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i9.TbErrorComponent, selector: "tb-error", inputs: ["error"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i7$5.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", exportAs: ["matAutocompleteTrigger"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { type: i7$5.MatAutocompleteOrigin, selector: "[matAutocompleteOrigin]", exportAs: ["matAutocompleteOrigin"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlDirective, selector: "[formControl]", inputs: ["disabled", "formControl", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe, "async": i10.AsyncPipe, "highlight": i12$1.HighlightPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: EntityDetailsConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-entity-details-config', + templateUrl: './entity-details-config.component.html', + styleUrls: ['./entity-details-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i4.TranslateService }, { type: i2.FormBuilder }]; }, propDecorators: { detailsInput: [{ + type: ViewChild, + args: ['detailsInput', { static: false }] + }] } }); + +class GetTelemetryFromDatabaseConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + this.aggregationTypes = AggregationType; + this.aggregations = Object.keys(AggregationType); + this.aggregationTypesTranslations = aggregationTranslations; + this.fetchMode = FetchMode; + this.fetchModes = Object.keys(FetchMode); + this.samplingOrders = Object.keys(SamplingOrder); + this.timeUnits = Object.values(TimeUnit); + this.timeUnitsTranslationMap = timeUnitTranslations; + } + configForm() { + return this.getTelemetryFromDatabaseConfigForm; + } + onConfigurationSet(configuration) { + this.getTelemetryFromDatabaseConfigForm = this.fb.group({ + latestTsKeyNames: [configuration ? configuration.latestTsKeyNames : null, []], + aggregation: [configuration ? configuration.aggregation : null, [Validators.required]], + fetchMode: [configuration ? configuration.fetchMode : null, [Validators.required]], + orderBy: [configuration ? configuration.orderBy : null, []], + limit: [configuration ? configuration.limit : null, []], + useMetadataIntervalPatterns: [configuration ? configuration.useMetadataIntervalPatterns : false, []], + startInterval: [configuration ? configuration.startInterval : null, []], + startIntervalTimeUnit: [configuration ? configuration.startIntervalTimeUnit : null, []], + endInterval: [configuration ? configuration.endInterval : null, []], + endIntervalTimeUnit: [configuration ? configuration.endIntervalTimeUnit : null, []], + startIntervalPattern: [configuration ? configuration.startIntervalPattern : null, []], + endIntervalPattern: [configuration ? configuration.endIntervalPattern : null, []], + }); + } + validatorTriggers() { + return ['fetchMode', 'useMetadataIntervalPatterns']; + } + updateValidators(emitEvent) { + const fetchMode = this.getTelemetryFromDatabaseConfigForm.get('fetchMode').value; + const useMetadataIntervalPatterns = this.getTelemetryFromDatabaseConfigForm.get('useMetadataIntervalPatterns').value; + if (fetchMode && fetchMode === FetchMode.ALL) { + this.getTelemetryFromDatabaseConfigForm.get('aggregation').setValidators([Validators.required]); + this.getTelemetryFromDatabaseConfigForm.get('orderBy').setValidators([Validators.required]); + this.getTelemetryFromDatabaseConfigForm.get('limit').setValidators([Validators.required, Validators.min(2), Validators.max(1000)]); + } + else { + this.getTelemetryFromDatabaseConfigForm.get('aggregation').setValidators([]); + this.getTelemetryFromDatabaseConfigForm.get('orderBy').setValidators([]); + this.getTelemetryFromDatabaseConfigForm.get('limit').setValidators([]); + } + if (useMetadataIntervalPatterns) { + 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([Validators.required]); + this.getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').setValidators([Validators.required]); + } + else { + this.getTelemetryFromDatabaseConfigForm.get('startInterval').setValidators([Validators.required, + Validators.min(1), Validators.max(2147483647)]); + this.getTelemetryFromDatabaseConfigForm.get('startIntervalTimeUnit').setValidators([Validators.required]); + this.getTelemetryFromDatabaseConfigForm.get('endInterval').setValidators([Validators.required, + Validators.min(1), Validators.max(2147483647)]); + this.getTelemetryFromDatabaseConfigForm.get('endIntervalTimeUnit').setValidators([Validators.required]); + this.getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').setValidators([]); + this.getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').setValidators([]); + } + this.getTelemetryFromDatabaseConfigForm.get('aggregation').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('orderBy').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('limit').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('startInterval').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('startIntervalTimeUnit').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('endInterval').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('endIntervalTimeUnit').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('startIntervalPattern').updateValueAndValidity({ emitEvent }); + this.getTelemetryFromDatabaseConfigForm.get('endIntervalPattern').updateValueAndValidity({ emitEvent }); + } + removeKey(key, keysField) { + const keys = this.getTelemetryFromDatabaseConfigForm.get(keysField).value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.getTelemetryFromDatabaseConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + addKey(event, keysField) { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let keys = this.getTelemetryFromDatabaseConfigForm.get(keysField).value; + if (!keys || keys.indexOf(value) === -1) { + if (!keys) { + keys = []; + } + keys.push(value); + this.getTelemetryFromDatabaseConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + if (input) { + input.value = ''; + } + } +} +GetTelemetryFromDatabaseConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GetTelemetryFromDatabaseConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +GetTelemetryFromDatabaseConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: GetTelemetryFromDatabaseConfigComponent, selector: "tb-enrichment-node-get-telemetry-from-database", usesInheritance: true, ngImport: i0, 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}\n"], components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GetTelemetryFromDatabaseConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-get-telemetry-from-database', + templateUrl: './get-telemetry-from-database-config.component.html', + styleUrls: ['./get-telemetry-from-database-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class OriginatorAttributesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + } + configForm() { + return this.originatorAttributesConfigForm; + } + onConfigurationSet(configuration) { + this.originatorAttributesConfigForm = this.fb.group({ + tellFailureIfAbsent: [configuration ? configuration.tellFailureIfAbsent : false, []], + clientAttributeNames: [configuration ? configuration.clientAttributeNames : null, []], + sharedAttributeNames: [configuration ? configuration.sharedAttributeNames : null, []], + serverAttributeNames: [configuration ? configuration.serverAttributeNames : null, []], + latestTsKeyNames: [configuration ? configuration.latestTsKeyNames : null, []], + getLatestValueWithTs: [configuration ? configuration.getLatestValueWithTs : false, []] + }); + } + removeKey(key, keysField) { + const keys = this.originatorAttributesConfigForm.get(keysField).value; + const index = keys.indexOf(key); + if (index >= 0) { + keys.splice(index, 1); + this.originatorAttributesConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + addKey(event, keysField) { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let keys = this.originatorAttributesConfigForm.get(keysField).value; + if (!keys || keys.indexOf(value) === -1) { + if (!keys) { + keys = []; + } + keys.push(value); + this.originatorAttributesConfigForm.get(keysField).setValue(keys, { emitEvent: true }); + } + } + if (input) { + input.value = ''; + } + } +} +OriginatorAttributesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: OriginatorAttributesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +OriginatorAttributesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: OriginatorAttributesConfigComponent, selector: "tb-enrichment-node-originator-attributes-config", usesInheritance: true, ngImport: i0, 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}\n"], components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: OriginatorAttributesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-originator-attributes-config', + templateUrl: './originator-attributes-config.component.html', + styleUrls: ['./originator-attributes-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class OriginatorFieldsConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.originatorFieldsConfigForm; + } + onConfigurationSet(configuration) { + this.originatorFieldsConfigForm = this.fb.group({ + fieldsMapping: [configuration ? configuration.fieldsMapping : null, [Validators.required]] + }); + } +} +OriginatorFieldsConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: OriginatorFieldsConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +OriginatorFieldsConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: OriginatorFieldsConfigComponent, selector: "tb-enrichment-node-originator-fields-config", usesInheritance: true, ngImport: i0, template: "
\n \n \n \n
\n", components: [{ type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: OriginatorFieldsConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-originator-fields-config', + templateUrl: './originator-fields-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RelatedAttributesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.relatedAttributesConfigForm; + } + onConfigurationSet(configuration) { + this.relatedAttributesConfigForm = this.fb.group({ + relationsQuery: [configuration ? configuration.relationsQuery : null, [Validators.required]], + telemetry: [configuration ? configuration.telemetry : false, []], + attrMapping: [configuration ? configuration.attrMapping : null, [Validators.required]] + }); + } +} +RelatedAttributesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RelatedAttributesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RelatedAttributesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RelatedAttributesConfigComponent, selector: "tb-enrichment-node-related-attributes-config", usesInheritance: true, ngImport: i0, template: "
\n \n \n \n \n \n {{ 'tb.rulenode.latest-telemetry' | translate }}\n \n \n \n
\n", components: [{ type: RelationsQueryConfigComponent, selector: "tb-relations-query-config", inputs: ["disabled", "required"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RelatedAttributesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-related-attributes-config', + templateUrl: './related-attributes-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class TenantAttributesConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.tenantAttributesConfigForm; + } + onConfigurationSet(configuration) { + this.tenantAttributesConfigForm = this.fb.group({ + telemetry: [configuration ? configuration.telemetry : false, []], + attrMapping: [configuration ? configuration.attrMapping : null, [Validators.required]] + }); + } +} +TenantAttributesConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: TenantAttributesConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +TenantAttributesConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: TenantAttributesConfigComponent, selector: "tb-enrichment-node-tenant-attributes-config", usesInheritance: true, ngImport: i0, template: "
\n \n \n {{ 'tb.rulenode.latest-telemetry' | translate }}\n \n \n \n
\n", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: KvMapConfigComponent, selector: "tb-kv-map-config", inputs: ["disabled", "requiredText", "keyText", "keyRequiredText", "valText", "valRequiredText", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: TenantAttributesConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-enrichment-node-tenant-attributes-config', + templateUrl: './tenant-attributes-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RulenodeCoreConfigEnrichmentModule { +} +RulenodeCoreConfigEnrichmentModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigEnrichmentModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +RulenodeCoreConfigEnrichmentModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigEnrichmentModule, declarations: [CustomerAttributesConfigComponent, + EntityDetailsConfigComponent, + DeviceAttributesConfigComponent, + OriginatorAttributesConfigComponent, + OriginatorFieldsConfigComponent, + GetTelemetryFromDatabaseConfigComponent, + RelatedAttributesConfigComponent, + TenantAttributesConfigComponent, + CalculateDeltaConfigComponent], imports: [CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule], exports: [CustomerAttributesConfigComponent, + EntityDetailsConfigComponent, + DeviceAttributesConfigComponent, + OriginatorAttributesConfigComponent, + OriginatorFieldsConfigComponent, + GetTelemetryFromDatabaseConfigComponent, + RelatedAttributesConfigComponent, + TenantAttributesConfigComponent, + CalculateDeltaConfigComponent] }); +RulenodeCoreConfigEnrichmentModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigEnrichmentModule, imports: [[ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ]] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigEnrichmentModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + CustomerAttributesConfigComponent, + EntityDetailsConfigComponent, + DeviceAttributesConfigComponent, + OriginatorAttributesConfigComponent, + OriginatorFieldsConfigComponent, + GetTelemetryFromDatabaseConfigComponent, + RelatedAttributesConfigComponent, + TenantAttributesConfigComponent, + CalculateDeltaConfigComponent + ], + imports: [ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ], + exports: [ + CustomerAttributesConfigComponent, + EntityDetailsConfigComponent, + DeviceAttributesConfigComponent, + OriginatorAttributesConfigComponent, + OriginatorFieldsConfigComponent, + GetTelemetryFromDatabaseConfigComponent, + RelatedAttributesConfigComponent, + TenantAttributesConfigComponent, + CalculateDeltaConfigComponent + ] + }] + }] }); + +class CheckAlarmStatusComponent extends RuleNodeConfigurationComponent { + constructor(store, translate, fb) { + super(store); + this.store = store; + this.translate = translate; + this.fb = fb; + this.alarmStatusTranslationsMap = alarmStatusTranslations; + this.alarmStatusList = []; + this.searchText = ''; + this.displayStatusFn = this.displayStatus.bind(this); + for (const field of Object.keys(AlarmStatus)) { + this.alarmStatusList.push(AlarmStatus[field]); + } + this.statusFormControl = new FormControl(''); + this.filteredAlarmStatus = this.statusFormControl.valueChanges + .pipe(startWith(''), map((value) => value ? value : ''), mergeMap(name => this.fetchAlarmStatus(name)), share()); + } + ngOnInit() { + super.ngOnInit(); + } + configForm() { + return this.alarmStatusConfigForm; + } + prepareInputConfig(configuration) { + this.searchText = ''; + this.statusFormControl.patchValue('', { emitEvent: true }); + return configuration; + } + onConfigurationSet(configuration) { + this.alarmStatusConfigForm = this.fb.group({ + alarmStatusList: [configuration ? configuration.alarmStatusList : null, [Validators.required]], + }); + } + displayStatus(status) { + return status ? this.translate.instant(alarmStatusTranslations.get(status)) : undefined; + } + fetchAlarmStatus(searchText) { + const alarmStatusList = this.getAlarmStatusList(); + this.searchText = searchText; + if (this.searchText && this.searchText.length) { + const search = this.searchText.toUpperCase(); + return of(alarmStatusList.filter(field => this.translate.instant(alarmStatusTranslations.get(AlarmStatus[field])).toUpperCase().includes(search))); + } + else { + return of(alarmStatusList); + } + } + alarmStatusSelected(event) { + this.addAlarmStatus(event.option.value); + this.clear(''); + } + removeAlarmStatus(status) { + const alarmStatusList = this.alarmStatusConfigForm.get('alarmStatusList').value; + if (alarmStatusList) { + const index = alarmStatusList.indexOf(status); + if (index >= 0) { + alarmStatusList.splice(index, 1); + this.alarmStatusConfigForm.get('alarmStatusList').setValue(alarmStatusList); + } + } + } + addAlarmStatus(status) { + let alarmStatusList = this.alarmStatusConfigForm.get('alarmStatusList').value; + if (!alarmStatusList) { + alarmStatusList = []; + } + const index = alarmStatusList.indexOf(status); + if (index === -1) { + alarmStatusList.push(status); + this.alarmStatusConfigForm.get('alarmStatusList').setValue(alarmStatusList); + } + } + getAlarmStatusList() { + return this.alarmStatusList.filter((listItem) => { + return this.alarmStatusConfigForm.get('alarmStatusList').value.indexOf(listItem) === -1; + }); + } + onAlarmStatusInputFocus() { + this.statusFormControl.updateValueAndValidity({ onlySelf: true, emitEvent: true }); + } + clear(value = '') { + this.alarmStatusInput.nativeElement.value = value; + this.statusFormControl.patchValue(null, { emitEvent: true }); + setTimeout(() => { + this.alarmStatusInput.nativeElement.blur(); + this.alarmStatusInput.nativeElement.focus(); + }, 0); + } +} +CheckAlarmStatusComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckAlarmStatusComponent, deps: [{ token: i1.Store }, { token: i4.TranslateService }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CheckAlarmStatusComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CheckAlarmStatusComponent, selector: "tb-filter-node-check-alarm-status-config", viewQueries: [{ propertyName: "alarmStatusInput", first: true, predicate: ["alarmStatusInput"], descendants: true }], usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: i7$5.MatAutocomplete, selector: "mat-autocomplete", inputs: ["disableRipple"], exportAs: ["matAutocomplete"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i9.TbErrorComponent, selector: "tb-error", inputs: ["error"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i7$5.MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", exportAs: ["matAutocompleteTrigger"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { type: i7$5.MatAutocompleteOrigin, selector: "[matAutocompleteOrigin]", exportAs: ["matAutocompleteOrigin"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlDirective, selector: "[formControl]", inputs: ["disabled", "formControl", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], pipes: { "translate": i4.TranslatePipe, "async": i10.AsyncPipe, "highlight": i12$1.HighlightPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckAlarmStatusComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-check-alarm-status-config', + templateUrl: './check-alarm-status.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i4.TranslateService }, { type: i2.FormBuilder }]; }, propDecorators: { alarmStatusInput: [{ + type: ViewChild, + args: ['alarmStatusInput', { static: false }] + }] } }); + +class CheckMessageConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; + } + configForm() { + return this.checkMessageConfigForm; + } + onConfigurationSet(configuration) { + this.checkMessageConfigForm = this.fb.group({ + messageNames: [configuration ? configuration.messageNames : null, []], + metadataNames: [configuration ? configuration.metadataNames : null, []], + checkAllKeys: [configuration ? configuration.checkAllKeys : false, []], + }); + } + validateConfig() { + const messageNames = this.checkMessageConfigForm.get('messageNames').value; + const metadataNames = this.checkMessageConfigForm.get('metadataNames').value; + return messageNames.length > 0 || metadataNames.length > 0; + } + removeMessageName(messageName) { + const messageNames = this.checkMessageConfigForm.get('messageNames').value; + const index = messageNames.indexOf(messageName); + if (index >= 0) { + messageNames.splice(index, 1); + this.checkMessageConfigForm.get('messageNames').setValue(messageNames, { emitEvent: true }); + } + } + removeMetadataName(metadataName) { + const metadataNames = this.checkMessageConfigForm.get('metadataNames').value; + const index = metadataNames.indexOf(metadataName); + if (index >= 0) { + metadataNames.splice(index, 1); + this.checkMessageConfigForm.get('metadataNames').setValue(metadataNames, { emitEvent: true }); + } + } + addMessageName(event) { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let messageNames = this.checkMessageConfigForm.get('messageNames').value; + if (!messageNames || messageNames.indexOf(value) === -1) { + if (!messageNames) { + messageNames = []; + } + messageNames.push(value); + this.checkMessageConfigForm.get('messageNames').setValue(messageNames, { emitEvent: true }); + } + } + if (input) { + input.value = ''; + } + } + addMetadataName(event) { + const input = event.input; + let value = event.value; + if ((value || '').trim()) { + value = value.trim(); + let metadataNames = this.checkMessageConfigForm.get('metadataNames').value; + if (!metadataNames || metadataNames.indexOf(value) === -1) { + if (!metadataNames) { + metadataNames = []; + } + metadataNames.push(value); + this.checkMessageConfigForm.get('metadataNames').setValue(metadataNames, { emitEvent: true }); + } + } + if (input) { + input.value = ''; + } + } +} +CheckMessageConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckMessageConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CheckMessageConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CheckMessageConfigComponent, selector: "tb-filter-node-check-message-config", usesInheritance: true, ngImport: i0, 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}\n"], components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i5$2.MatChipList, selector: "mat-chip-list", inputs: ["aria-orientation", "multiple", "compareWith", "value", "required", "placeholder", "disabled", "selectable", "tabIndex", "errorStateMatcher"], outputs: ["change", "valueChange"], exportAs: ["matChipList"] }, { type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5$2.MatChip, selector: "mat-basic-chip, [mat-basic-chip], mat-chip, [mat-chip]", inputs: ["color", "disableRipple", "tabIndex", "selected", "value", "selectable", "disabled", "removable"], outputs: ["selectionChange", "destroyed", "removed"], exportAs: ["matChip"] }, { type: i5$2.MatChipRemove, selector: "[matChipRemove]" }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i5$2.MatChipInput, selector: "input[matChipInputFor]", inputs: ["matChipInputSeparatorKeyCodes", "placeholder", "id", "matChipInputFor", "matChipInputAddOnBlur", "disabled"], outputs: ["matChipInputTokenEnd"], exportAs: ["matChipInput", "matChipInputFor"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckMessageConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-check-message-config', + templateUrl: './check-message-config.component.html', + styleUrls: ['./check-message-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class CheckRelationConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.entitySearchDirection = Object.keys(EntitySearchDirection); + this.entitySearchDirectionTranslationsMap = entitySearchDirectionTranslations; + } + configForm() { + return this.checkRelationConfigForm; + } + onConfigurationSet(configuration) { + this.checkRelationConfigForm = this.fb.group({ + checkForSingleEntity: [configuration ? configuration.checkForSingleEntity : false, []], + direction: [configuration ? configuration.direction : null, []], + entityType: [configuration ? configuration.entityType : null, + configuration && configuration.checkForSingleEntity ? [Validators.required] : []], + entityId: [configuration ? configuration.entityId : null, + configuration && configuration.checkForSingleEntity ? [Validators.required] : []], + relationType: [configuration ? configuration.relationType : null, [Validators.required]] + }); + } + validatorTriggers() { + return ['checkForSingleEntity']; + } + updateValidators(emitEvent) { + const checkForSingleEntity = this.checkRelationConfigForm.get('checkForSingleEntity').value; + this.checkRelationConfigForm.get('entityType').setValidators(checkForSingleEntity ? [Validators.required] : []); + this.checkRelationConfigForm.get('entityType').updateValueAndValidity({ emitEvent }); + this.checkRelationConfigForm.get('entityId').setValidators(checkForSingleEntity ? [Validators.required] : []); + this.checkRelationConfigForm.get('entityId').updateValueAndValidity({ emitEvent }); + } +} +CheckRelationConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckRelationConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +CheckRelationConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: CheckRelationConfigComponent, selector: "tb-filter-node-check-relation-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: i7$1.EntityTypeSelectComponent, selector: "tb-entity-type-select", inputs: ["allowedEntityTypes", "useAliasEntityTypes", "showLabel", "required", "disabled"] }, { type: i8$3.EntityAutocompleteComponent, selector: "tb-entity-autocomplete", inputs: ["entityType", "entitySubtype", "excludeEntityIds", "labelText", "requiredText", "required", "disabled"], outputs: ["entityChanged"] }, { type: i7$3.RelationTypeAutocompleteComponent, selector: "tb-relation-type-autocomplete", inputs: ["required", "disabled"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: CheckRelationConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-check-relation-config', + templateUrl: './check-relation-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class GpsGeoFilterConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.perimeterType = PerimeterType; + this.perimeterTypes = Object.keys(PerimeterType); + this.perimeterTypeTranslationMap = perimeterTypeTranslations; + this.rangeUnits = Object.keys(RangeUnit); + this.rangeUnitTranslationMap = rangeUnitTranslations; + } + configForm() { + return this.geoFilterConfigForm; + } + onConfigurationSet(configuration) { + this.geoFilterConfigForm = this.fb.group({ + latitudeKeyName: [configuration ? configuration.latitudeKeyName : null, [Validators.required]], + longitudeKeyName: [configuration ? configuration.longitudeKeyName : null, [Validators.required]], + fetchPerimeterInfoFromMessageMetadata: [configuration ? configuration.fetchPerimeterInfoFromMessageMetadata : false, []], + perimeterType: [configuration ? configuration.perimeterType : null, []], + centerLatitude: [configuration ? configuration.centerLatitude : null, []], + centerLongitude: [configuration ? configuration.centerLatitude : null, []], + range: [configuration ? configuration.range : null, []], + rangeUnit: [configuration ? configuration.rangeUnit : null, []], + polygonsDefinition: [configuration ? configuration.polygonsDefinition : null, []] + }); + } + validatorTriggers() { + return ['fetchPerimeterInfoFromMessageMetadata', 'perimeterType']; + } + updateValidators(emitEvent) { + const fetchPerimeterInfoFromMessageMetadata = this.geoFilterConfigForm.get('fetchPerimeterInfoFromMessageMetadata').value; + const perimeterType = this.geoFilterConfigForm.get('perimeterType').value; + if (fetchPerimeterInfoFromMessageMetadata) { + this.geoFilterConfigForm.get('perimeterType').setValidators([]); + } + else { + this.geoFilterConfigForm.get('perimeterType').setValidators([Validators.required]); + } + if (!fetchPerimeterInfoFromMessageMetadata && perimeterType === PerimeterType.CIRCLE) { + this.geoFilterConfigForm.get('centerLatitude').setValidators([Validators.required, + Validators.min(-90), Validators.max(90)]); + this.geoFilterConfigForm.get('centerLongitude').setValidators([Validators.required, + Validators.min(-180), Validators.max(180)]); + this.geoFilterConfigForm.get('range').setValidators([Validators.required, Validators.min(0)]); + this.geoFilterConfigForm.get('rangeUnit').setValidators([Validators.required]); + } + else { + this.geoFilterConfigForm.get('centerLatitude').setValidators([]); + this.geoFilterConfigForm.get('centerLongitude').setValidators([]); + this.geoFilterConfigForm.get('range').setValidators([]); + this.geoFilterConfigForm.get('rangeUnit').setValidators([]); + } + if (!fetchPerimeterInfoFromMessageMetadata && perimeterType === PerimeterType.POLYGON) { + this.geoFilterConfigForm.get('polygonsDefinition').setValidators([Validators.required]); + } + else { + this.geoFilterConfigForm.get('polygonsDefinition').setValidators([]); + } + this.geoFilterConfigForm.get('perimeterType').updateValueAndValidity({ emitEvent: false }); + this.geoFilterConfigForm.get('centerLatitude').updateValueAndValidity({ emitEvent }); + this.geoFilterConfigForm.get('centerLongitude').updateValueAndValidity({ emitEvent }); + this.geoFilterConfigForm.get('range').updateValueAndValidity({ emitEvent }); + this.geoFilterConfigForm.get('rangeUnit').updateValueAndValidity({ emitEvent }); + this.geoFilterConfigForm.get('polygonsDefinition').updateValueAndValidity({ emitEvent }); + } +} +GpsGeoFilterConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GpsGeoFilterConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +GpsGeoFilterConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: GpsGeoFilterConfigComponent, selector: "tb-filter-node-gps-geofencing-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i3$1.MatCheckbox, selector: "mat-checkbox", inputs: ["disableRipple", "color", "tabIndex", "aria-label", "aria-labelledby", "id", "labelPosition", "name", "required", "checked", "disabled", "indeterminate", "aria-describedby", "value"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i8.DefaultLayoutGapDirective, selector: " [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg]", inputs: ["fxLayoutGap", "fxLayoutGap.xs", "fxLayoutGap.sm", "fxLayoutGap.md", "fxLayoutGap.lg", "fxLayoutGap.xl", "fxLayoutGap.lt-sm", "fxLayoutGap.lt-md", "fxLayoutGap.lt-lg", "fxLayoutGap.lt-xl", "fxLayoutGap.gt-xs", "fxLayoutGap.gt-sm", "fxLayoutGap.gt-md", "fxLayoutGap.gt-lg"] }, { type: i2.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { type: i2.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { type: i2.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: GpsGeoFilterConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-gps-geofencing-config', + templateUrl: './gps-geo-filter-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class MessageTypeConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.messageTypeConfigForm; + } + onConfigurationSet(configuration) { + this.messageTypeConfigForm = this.fb.group({ + messageTypes: [configuration ? configuration.messageTypes : null, [Validators.required]] + }); + } +} +MessageTypeConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MessageTypeConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +MessageTypeConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: MessageTypeConfigComponent, selector: "tb-filter-node-message-type-config", usesInheritance: true, ngImport: i0, template: "
\n \n
\n", components: [{ type: MessageTypesConfigComponent, selector: "tb-message-types-config", inputs: ["required", "label", "placeholder", "disabled"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: MessageTypeConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-message-type-config', + templateUrl: './message-type-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class OriginatorTypeConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.allowedEntityTypes = [ + EntityType.DEVICE, + EntityType.ASSET, + EntityType.ENTITY_VIEW, + EntityType.TENANT, + EntityType.CUSTOMER, + EntityType.USER, + EntityType.DASHBOARD, + EntityType.RULE_CHAIN, + EntityType.RULE_NODE + ]; + } + configForm() { + return this.originatorTypeConfigForm; + } + onConfigurationSet(configuration) { + this.originatorTypeConfigForm = this.fb.group({ + originatorTypes: [configuration ? configuration.originatorTypes : null, [Validators.required]] + }); + } +} +OriginatorTypeConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: OriginatorTypeConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +OriginatorTypeConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: OriginatorTypeConfigComponent, selector: "tb-filter-node-originator-type-config", usesInheritance: true, ngImport: i0, 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}\n"], components: [{ type: i3$5.EntityTypeListComponent, selector: "tb-entity-type-list", inputs: ["required", "disabled", "allowedEntityTypes", "ignoreAuthorityFilter"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i8.DefaultFlexDirective, selector: " [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg]", inputs: ["fxFlex", "fxFlex.xs", "fxFlex.sm", "fxFlex.md", "fxFlex.lg", "fxFlex.xl", "fxFlex.lt-sm", "fxFlex.lt-md", "fxFlex.lt-lg", "fxFlex.lt-xl", "fxFlex.gt-xs", "fxFlex.gt-sm", "fxFlex.gt-md", "fxFlex.gt-lg"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: OriginatorTypeConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-originator-type-config', + templateUrl: './originator-type-config.component.html', + styleUrls: ['./originator-type-config.component.scss'] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class ScriptConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + } + configForm() { + return this.scriptConfigForm; + } + onConfigurationSet(configuration) { + this.scriptConfigForm = this.fb.group({ + jsScript: [configuration ? configuration.jsScript : null, [Validators.required]] + }); + } + testScript() { + const script = this.scriptConfigForm.get('jsScript').value; + this.nodeScriptTestService.testNodeScript(script, 'filter', this.translate.instant('tb.rulenode.filter'), 'Filter', ['msg', 'metadata', 'msgType'], this.ruleNodeId, 'rulenode/filter_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.scriptConfigForm.get('jsScript').setValue(theScript); + } + }); + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +ScriptConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ScriptConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +ScriptConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: ScriptConfigComponent, selector: "tb-filter-node-script-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "
\n \n \n \n
\n \n
\n
\n", components: [{ type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ScriptConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-script-config', + templateUrl: './script-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class SwitchConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + } + configForm() { + return this.switchConfigForm; + } + onConfigurationSet(configuration) { + this.switchConfigForm = this.fb.group({ + jsScript: [configuration ? configuration.jsScript : null, [Validators.required]] + }); + } + testScript() { + const script = this.switchConfigForm.get('jsScript').value; + this.nodeScriptTestService.testNodeScript(script, 'switch', this.translate.instant('tb.rulenode.switch'), 'Switch', ['msg', 'metadata', 'msgType'], this.ruleNodeId, 'rulenode/switch_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.switchConfigForm.get('jsScript').setValue(theScript); + } + }); + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +SwitchConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SwitchConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +SwitchConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: SwitchConfigComponent, selector: "tb-filter-node-switch-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "
\n \n \n \n
\n \n
\n
\n", components: [{ type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: SwitchConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-filter-node-switch-config', + templateUrl: './switch-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class RuleNodeCoreConfigFilterModule { +} +RuleNodeCoreConfigFilterModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFilterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +RuleNodeCoreConfigFilterModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFilterModule, declarations: [CheckMessageConfigComponent, + CheckRelationConfigComponent, + GpsGeoFilterConfigComponent, + MessageTypeConfigComponent, + OriginatorTypeConfigComponent, + ScriptConfigComponent, + SwitchConfigComponent, + CheckAlarmStatusComponent], imports: [CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule], exports: [CheckMessageConfigComponent, + CheckRelationConfigComponent, + GpsGeoFilterConfigComponent, + MessageTypeConfigComponent, + OriginatorTypeConfigComponent, + ScriptConfigComponent, + SwitchConfigComponent, + CheckAlarmStatusComponent] }); +RuleNodeCoreConfigFilterModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFilterModule, imports: [[ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ]] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFilterModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + CheckMessageConfigComponent, + CheckRelationConfigComponent, + GpsGeoFilterConfigComponent, + MessageTypeConfigComponent, + OriginatorTypeConfigComponent, + ScriptConfigComponent, + SwitchConfigComponent, + CheckAlarmStatusComponent + ], + imports: [ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ], + exports: [ + CheckMessageConfigComponent, + CheckRelationConfigComponent, + GpsGeoFilterConfigComponent, + MessageTypeConfigComponent, + OriginatorTypeConfigComponent, + ScriptConfigComponent, + SwitchConfigComponent, + CheckAlarmStatusComponent + ] + }] + }] }); + +class ChangeOriginatorConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.originatorSource = OriginatorSource; + this.originatorSources = Object.keys(OriginatorSource); + this.originatorSourceTranslationMap = originatorSourceTranslations; + } + configForm() { + return this.changeOriginatorConfigForm; + } + onConfigurationSet(configuration) { + this.changeOriginatorConfigForm = this.fb.group({ + originatorSource: [configuration ? configuration.originatorSource : null, [Validators.required]], + relationsQuery: [configuration ? configuration.relationsQuery : null, []] + }); + } + validatorTriggers() { + return ['originatorSource']; + } + updateValidators(emitEvent) { + const originatorSource = this.changeOriginatorConfigForm.get('originatorSource').value; + if (originatorSource && originatorSource === OriginatorSource.RELATED) { + this.changeOriginatorConfigForm.get('relationsQuery').setValidators([Validators.required]); + } + else { + this.changeOriginatorConfigForm.get('relationsQuery').setValidators([]); + } + this.changeOriginatorConfigForm.get('relationsQuery').updateValueAndValidity({ emitEvent }); + } +} +ChangeOriginatorConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ChangeOriginatorConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +ChangeOriginatorConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: ChangeOriginatorConfigComponent, selector: "tb-transformation-node-change-originator-config", usesInheritance: true, ngImport: i0, template: "
\n \n tb.rulenode.originator-source\n \n \n {{ originatorSourceTranslationMap.get(source) | translate }}\n \n \n \n
\n \n \n \n
\n
\n", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { type: RelationsQueryConfigComponent, selector: "tb-relations-query-config", inputs: ["disabled", "required"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ChangeOriginatorConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-transformation-node-change-originator-config', + templateUrl: './change-originator-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class TransformScriptConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb, nodeScriptTestService, translate) { + super(store); + this.store = store; + this.fb = fb; + this.nodeScriptTestService = nodeScriptTestService; + this.translate = translate; + } + configForm() { + return this.scriptConfigForm; + } + onConfigurationSet(configuration) { + this.scriptConfigForm = this.fb.group({ + jsScript: [configuration ? configuration.jsScript : null, [Validators.required]] + }); + } + testScript() { + const script = this.scriptConfigForm.get('jsScript').value; + this.nodeScriptTestService.testNodeScript(script, 'update', this.translate.instant('tb.rulenode.transformer'), 'Transform', ['msg', 'metadata', 'msgType'], this.ruleNodeId, 'rulenode/transformation_node_script_fn').subscribe((theScript) => { + if (theScript) { + this.scriptConfigForm.get('jsScript').setValue(theScript); + } + }); + } + onValidate() { + this.jsFuncComponent.validateOnSubmit(); + } +} +TransformScriptConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: TransformScriptConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }, { token: i3$3.NodeScriptTestService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); +TransformScriptConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: TransformScriptConfigComponent, selector: "tb-transformation-node-script-config", viewQueries: [{ propertyName: "jsFuncComponent", first: true, predicate: ["jsFuncComponent"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "
\n \n \n \n
\n \n
\n
\n", components: [{ type: i5$1.JsFuncComponent, selector: "tb-js-func", inputs: ["functionName", "functionArgs", "validationArgs", "resultType", "disabled", "fillHeight", "editorCompleter", "globalVariables", "disableUndefinedCheck", "helpId", "noValidate", "required"] }, { type: i6.MatButton, selector: "button[mat-button], button[mat-raised-button], button[mat-icon-button], button[mat-fab], button[mat-mini-fab], button[mat-stroked-button], button[mat-flat-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: TransformScriptConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-transformation-node-script-config', + templateUrl: './script-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }, { type: i3$3.NodeScriptTestService }, { type: i4.TranslateService }]; }, propDecorators: { jsFuncComponent: [{ + type: ViewChild, + args: ['jsFuncComponent', { static: true }] + }] } }); + +class ToEmailConfigComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.mailBodyTypes = [ + { name: 'tb.mail-body-type.plain-text', value: 'false' }, + { name: 'tb.mail-body-type.html', value: 'true' }, + { name: 'tb.mail-body-type.dynamic', value: 'dynamic' } + ]; + } + configForm() { + return this.toEmailConfigForm; + } + onConfigurationSet(configuration) { + this.toEmailConfigForm = this.fb.group({ + fromTemplate: [configuration ? configuration.fromTemplate : null, [Validators.required]], + toTemplate: [configuration ? configuration.toTemplate : null, [Validators.required]], + ccTemplate: [configuration ? configuration.ccTemplate : null, []], + bccTemplate: [configuration ? configuration.bccTemplate : null, []], + subjectTemplate: [configuration ? configuration.subjectTemplate : null, [Validators.required]], + mailBodyType: [configuration ? configuration.mailBodyType : null], + isHtmlTemplate: [configuration ? configuration.isHtmlTemplate : null], + bodyTemplate: [configuration ? configuration.bodyTemplate : null, [Validators.required]], + }); + this.toEmailConfigForm.get('mailBodyType').valueChanges.pipe(startWith([configuration === null || configuration === void 0 ? void 0 : configuration.subjectTemplate])).subscribe((mailBodyType) => { + if (mailBodyType === 'dynamic') { + this.toEmailConfigForm.get('isHtmlTemplate').patchValue('', { emitEvent: false }); + this.toEmailConfigForm.get('isHtmlTemplate').setValidators(Validators.required); + } + else { + this.toEmailConfigForm.get('isHtmlTemplate').clearValidators(); + } + this.toEmailConfigForm.get('isHtmlTemplate').updateValueAndValidity(); + }); + } +} +ToEmailConfigComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ToEmailConfigComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +ToEmailConfigComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: ToEmailConfigComponent, selector: "tb-transformation-node-to-email-config", usesInheritance: true, ngImport: i0, 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", components: [{ type: i3.MatFormField, selector: "mat-form-field", inputs: ["color", "floatLabel", "appearance", "hideRequiredMarker", "hintLabel"], exportAs: ["matFormField"] }, { type: i4$1.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex"], exportAs: ["matSelect"] }, { type: i5.MatOption, selector: "mat-option", exportAs: ["matOption"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i3.MatLabel, selector: "mat-label" }, { type: i4.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { type: i11.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["id", "disabled", "required", "type", "value", "readonly", "placeholder", "errorStateMatcher", "aria-describedby"], exportAs: ["matInput"] }, { type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }, { type: i10.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.MatError, selector: "mat-error", inputs: ["id"] }, { type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { type: i10.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], pipes: { "translate": i4.TranslatePipe, "safeHtml": SafeHtmlPipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: ToEmailConfigComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-transformation-node-to-email-config', + templateUrl: './to-email-config.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RulenodeCoreConfigTransformModule { +} +RulenodeCoreConfigTransformModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigTransformModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +RulenodeCoreConfigTransformModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigTransformModule, declarations: [ChangeOriginatorConfigComponent, + TransformScriptConfigComponent, + ToEmailConfigComponent], imports: [CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule], exports: [ChangeOriginatorConfigComponent, + TransformScriptConfigComponent, + ToEmailConfigComponent] }); +RulenodeCoreConfigTransformModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigTransformModule, imports: [[ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ]] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RulenodeCoreConfigTransformModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + ChangeOriginatorConfigComponent, + TransformScriptConfigComponent, + ToEmailConfigComponent + ], + imports: [ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ], + exports: [ + ChangeOriginatorConfigComponent, + TransformScriptConfigComponent, + ToEmailConfigComponent + ] + }] + }] }); + +class RuleChainInputComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + this.entityType = EntityType; + } + configForm() { + return this.ruleChainInputConfigForm; + } + onConfigurationSet(configuration) { + this.ruleChainInputConfigForm = this.fb.group({ + ruleChainId: [configuration ? configuration.ruleChainId : null, [Validators.required]] + }); + } +} +RuleChainInputComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleChainInputComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RuleChainInputComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RuleChainInputComponent, selector: "tb-flow-node-rule-chain-input-config", usesInheritance: true, ngImport: i0, template: "
\n \n \n
\n", components: [{ type: i8$3.EntityAutocompleteComponent, selector: "tb-entity-autocomplete", inputs: ["entityType", "entitySubtype", "excludeEntityIds", "labelText", "requiredText", "required", "disabled"], outputs: ["entityChanged"] }], directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i2.FormControlName, selector: "[formControlName]", inputs: ["disabled", "formControlName", "ngModel"], outputs: ["ngModelChange"] }] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleChainInputComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-flow-node-rule-chain-input-config', + templateUrl: './rule-chain-input.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RuleChainOutputComponent extends RuleNodeConfigurationComponent { + constructor(store, fb) { + super(store); + this.store = store; + this.fb = fb; + } + configForm() { + return this.ruleChainOutputConfigForm; + } + onConfigurationSet(configuration) { + this.ruleChainOutputConfigForm = this.fb.group({}); + } +} +RuleChainOutputComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleChainOutputComponent, deps: [{ token: i1.Store }, { token: i2.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); +RuleChainOutputComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "12.2.14", type: RuleChainOutputComponent, selector: "tb-flow-node-rule-chain-output-config", usesInheritance: true, ngImport: i0, template: "
\n
\n
\n", directives: [{ type: i8.DefaultLayoutDirective, selector: " [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg]", inputs: ["fxLayout", "fxLayout.xs", "fxLayout.sm", "fxLayout.md", "fxLayout.lg", "fxLayout.xl", "fxLayout.lt-sm", "fxLayout.lt-md", "fxLayout.lt-lg", "fxLayout.lt-xl", "fxLayout.gt-xs", "fxLayout.gt-sm", "fxLayout.gt-md", "fxLayout.gt-lg"] }, { type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], pipes: { "translate": i4.TranslatePipe } }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleChainOutputComponent, decorators: [{ + type: Component, + args: [{ + selector: 'tb-flow-node-rule-chain-output-config', + templateUrl: './rule-chain-output.component.html', + styleUrls: [] + }] + }], ctorParameters: function () { return [{ type: i1.Store }, { type: i2.FormBuilder }]; } }); + +class RuleNodeCoreConfigFlowModule { +} +RuleNodeCoreConfigFlowModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFlowModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); +RuleNodeCoreConfigFlowModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFlowModule, declarations: [RuleChainInputComponent, + RuleChainOutputComponent], imports: [CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule], exports: [RuleChainInputComponent, + RuleChainOutputComponent] }); +RuleNodeCoreConfigFlowModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFlowModule, imports: [[ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ]] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigFlowModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + RuleChainInputComponent, + RuleChainOutputComponent + ], + imports: [ + CommonModule, + SharedModule, + RulenodeCoreConfigCommonModule + ], + exports: [ + RuleChainInputComponent, + RuleChainOutputComponent + ] + }] + }] }); + +function addRuleNodeCoreLocaleEnglish(translate) { + const enUS = { + 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', + 'ignore-request-body': 'Without request body', + 'read-timeout': 'Read timeout in millis', + 'read-timeout-hint': 'The value of 0 means an infinite timeout', + 'max-parallel-requests-count': 'Max number of parallel requests', + 'max-parallel-requests-count-hint': 'The value of 0 specifies no limit in parallel processing', + headers: 'Headers', + 'headers-hint': 'Use ${metadataKey} ' + + 'for value from metadata, $[messageKey] ' + + 'for value from message body in header/value fields', + header: 'Header', + 'header-required': 'Header is required', + value: 'Value', + 'value-required': 'Value is required', + 'topic-pattern': 'Topic pattern', + 'topic-pattern-required': 'Topic pattern is required', + topic: 'Topic', + 'topic-required': 'Topic is required', + 'bootstrap-servers': 'Bootstrap servers', + 'bootstrap-servers-required': 'Bootstrap servers value is required', + 'other-properties': 'Other properties', + key: 'Key', + 'key-required': 'Key is required', + retries: 'Automatically retry times if fails', + 'min-retries-message': 'Only 0 minimum retries is allowed.', + 'batch-size-bytes': 'Produces batch size in bytes', + 'min-batch-size-bytes-message': 'Only 0 minimum batch size is allowed.', + 'linger-ms': 'Time to buffer locally (ms)', + 'min-linger-ms-message': 'Only 0 ms minimum value is allowed.', + 'buffer-memory-bytes': 'Client buffer max size in bytes', + 'min-buffer-memory-message': 'Only 0 minimum buffer size is allowed.', + acks: 'Number of acknowledgments', + 'key-serializer': 'Key serializer', + 'key-serializer-required': 'Key serializer is required', + 'value-serializer': 'Value serializer', + 'value-serializer-required': 'Value serializer is required', + 'topic-arn-pattern': 'Topic ARN pattern', + 'topic-arn-pattern-required': 'Topic ARN pattern is required', + 'aws-access-key-id': 'AWS Access Key ID', + 'aws-access-key-id-required': 'AWS Access Key ID is required', + 'aws-secret-access-key': 'AWS Secret Access Key', + 'aws-secret-access-key-required': 'AWS Secret Access Key is required', + 'aws-region': 'AWS Region', + 'aws-region-required': 'AWS Region is required', + 'exchange-name-pattern': 'Exchange name pattern', + 'routing-key-pattern': 'Routing key pattern', + 'message-properties': 'Message properties', + host: 'Host', + 'host-required': 'Host is required', + port: 'Port', + 'port-required': 'Port is required', + 'port-range': 'Port should be in a range from 1 to 65535.', + 'virtual-host': 'Virtual host', + username: 'Username', + password: 'Password', + 'automatic-recovery': 'Automatic recovery', + 'connection-timeout-ms': 'Connection timeout (ms)', + 'min-connection-timeout-ms-message': 'Only 0 ms minimum value is allowed.', + 'handshake-timeout-ms': 'Handshake timeout (ms)', + 'min-handshake-timeout-ms-message': 'Only 0 ms minimum value is allowed.', + 'client-properties': 'Client properties', + 'queue-url-pattern': 'Queue URL pattern', + 'queue-url-pattern-required': 'Queue URL pattern is required', + 'delay-seconds': 'Delay (seconds)', + 'min-delay-seconds-message': 'Only 0 seconds minimum value is allowed.', + 'max-delay-seconds-message': 'Only 900 seconds maximum value is allowed.', + name: 'Name', + 'name-required': 'Name is required', + 'queue-type': 'Queue type', + 'sqs-queue-standard': 'Standard', + 'sqs-queue-fifo': 'FIFO', + 'gcp-project-id': 'GCP project ID', + 'gcp-project-id-required': 'GCP project ID is required', + 'gcp-service-account-key': 'GCP service account key file', + 'gcp-service-account-key-required': 'GCP service account key file is required', + 'pubsub-topic-name': 'Topic name', + 'pubsub-topic-name-required': 'Topic name is required', + 'message-attributes': 'Message attributes', + 'message-attributes-hint': 'Use ${metadataKey} ' + + 'for value from metadata, $[messageKey] ' + + 'for value from message body in name/value fields', + 'connect-timeout': 'Connection timeout (sec)', + 'connect-timeout-required': 'Connection timeout is required.', + 'connect-timeout-range': 'Connection timeout should be in a range from 1 to 200.', + 'client-id': 'Client ID', + 'client-id-hint': 'Hint: Optional. Leave empty for auto-generated client id. Be careful when specifying the Client ID.' + + 'Majority of the MQTT brokers will not allow multiple connections with the same client id. ' + + 'To connect to such brokers, your mqtt client id must be unique. ' + + 'When platform is running in a micro-services mode, the copy of rule nodeis launched in each micro-service. ' + + 'This will automatically lead to multiple mqtt clients with the same id and may cause failures of the rule node.', + '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-city': 'City', + 'entity-details-zip': 'Zip', + 'entity-details-address': 'Address', + 'entity-details-address2': 'Address2', + 'entity-details-additional_info': 'Additional Info', + 'entity-details-phone': 'Phone', + 'entity-details-email': 'Email', + '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.)', + 'output-node-name-hint': 'The rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain.' + }, + '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' + } + } + }; + translate.setTranslation('en_US', enUS, true); +} + +class RuleNodeCoreConfigModule { + constructor(translate) { + addRuleNodeCoreLocaleEnglish(translate); + } +} +RuleNodeCoreConfigModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigModule, deps: [{ token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.NgModule }); +RuleNodeCoreConfigModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigModule, declarations: [EmptyConfigComponent], imports: [CommonModule, + SharedModule], exports: [RuleNodeCoreConfigActionModule, + RuleNodeCoreConfigFilterModule, + RulenodeCoreConfigEnrichmentModule, + RulenodeCoreConfigTransformModule, + RuleNodeCoreConfigFlowModule, + EmptyConfigComponent] }); +RuleNodeCoreConfigModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigModule, imports: [[ + CommonModule, + SharedModule + ], RuleNodeCoreConfigActionModule, + RuleNodeCoreConfigFilterModule, + RulenodeCoreConfigEnrichmentModule, + RulenodeCoreConfigTransformModule, + RuleNodeCoreConfigFlowModule] }); +i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.14", ngImport: i0, type: RuleNodeCoreConfigModule, decorators: [{ + type: NgModule, + args: [{ + declarations: [ + EmptyConfigComponent + ], + imports: [ + CommonModule, + SharedModule + ], + exports: [ + RuleNodeCoreConfigActionModule, + RuleNodeCoreConfigFilterModule, + RulenodeCoreConfigEnrichmentModule, + RulenodeCoreConfigTransformModule, + RuleNodeCoreConfigFlowModule, + EmptyConfigComponent + ] + }] + }], ctorParameters: function () { return [{ type: i4.TranslateService }]; } }); + +/* + * Public API Surface of rule-core-config + */ + +/** + * Generated bundle index. Do not edit. + */ + +export { AssignCustomerConfigComponent, AttributesConfigComponent, AzureIotHubConfigComponent, CalculateDeltaConfigComponent, ChangeOriginatorConfigComponent, CheckAlarmStatusComponent, CheckMessageConfigComponent, CheckPointConfigComponent, CheckRelationConfigComponent, ClearAlarmConfigComponent, CreateAlarmConfigComponent, CreateRelationConfigComponent, CredentialsConfigComponent, CustomerAttributesConfigComponent, DeleteRelationConfigComponent, DeviceAttributesConfigComponent, DeviceProfileConfigComponent, DeviceRelationsQueryConfigComponent, EmptyConfigComponent, EntityDetailsConfigComponent, GeneratorConfigComponent, GetTelemetryFromDatabaseConfigComponent, GpsGeoActionConfigComponent, GpsGeoFilterConfigComponent, KafkaConfigComponent, KvMapConfigComponent, LogConfigComponent, MessageTypeConfigComponent, MessageTypesConfigComponent, MqttConfigComponent, MsgCountConfigComponent, MsgDelayConfigComponent, OriginatorAttributesConfigComponent, OriginatorFieldsConfigComponent, OriginatorTypeConfigComponent, PubSubConfigComponent, PushToCloudConfigComponent, PushToEdgeConfigComponent, RabbitMqConfigComponent, RelatedAttributesConfigComponent, RelationsQueryConfigComponent, RestApiCallConfigComponent, RpcReplyConfigComponent, RpcRequestConfigComponent, RuleChainInputComponent, RuleChainOutputComponent, RuleNodeCoreConfigActionModule, RuleNodeCoreConfigFilterModule, RuleNodeCoreConfigFlowModule, RuleNodeCoreConfigModule, RulenodeCoreConfigCommonModule, RulenodeCoreConfigEnrichmentModule, RulenodeCoreConfigTransformModule, SafeHtmlPipe, SaveToCustomTableConfigComponent, ScriptConfigComponent, SendEmailConfigComponent, SendSmsConfigComponent, SnsConfigComponent, SqsConfigComponent, SwitchConfigComponent, TenantAttributesConfigComponent, TimeseriesConfigComponent, ToEmailConfigComponent, TransformScriptConfigComponent, UnassignCustomerConfigComponent }; +//# sourceMappingURL=rulenode-core-config.js.map diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java new file mode 100644 index 0000000000..b1e96f228e --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbCreateRelationNodeTest.java @@ -0,0 +1,227 @@ +/** + * 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.action; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.common.util.ListeningExecutor; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.rule.engine.api.TbRelationTypes; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.relation.RelationService; + +import java.util.Collections; +import java.util.concurrent.Callable; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TbCreateRelationNodeTest { + + private static final String RELATION_TYPE_CONTAINS = "Contains"; + + private TbCreateRelationNode node; + + @Mock + private TbContext ctx; + @Mock + private AssetService assetService; + @Mock + private RelationService relationService; + + private TbMsg msg; + + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + + private ListeningExecutor dbExecutor; + + @Before + public void before() { + dbExecutor = new ListeningExecutor() { + @Override + public ListenableFuture executeAsync(Callable task) { + try { + return Futures.immediateFuture(task.call()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void execute(Runnable command) { + command.run(); + } + }; + } + + @Test + public void testCreateNewRelation() throws TbNodeException { + init(createRelationNodeConfig()); + + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + + AssetId assetId = new AssetId(Uuids.timeBased()); + Asset asset = new Asset(); + asset.setId(assetId); + + when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset); + when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset)); + + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("name", "AssetName"); + metaData.putValue("type", "AssetType"); + msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + + when(ctx.getRelationService().checkRelation(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + .thenReturn(Futures.immediateFuture(false)); + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + .thenReturn(Futures.immediateFuture(true)); + + node.onMsg(ctx, msg); + verify(ctx).tellNext(msg, TbRelationTypes.SUCCESS); + } + + @Test + public void testDeleteCurrentRelationsCreateNewRelation() throws TbNodeException { + init(createRelationNodeConfigWithRemoveCurrentRelations()); + + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + + AssetId assetId = new AssetId(Uuids.timeBased()); + Asset asset = new Asset(); + asset.setId(assetId); + + when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset); + when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset)); + + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("name", "AssetName"); + metaData.putValue("type", "AssetType"); + msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + + EntityRelation relation = new EntityRelation(); + when(ctx.getRelationService().findByToAndTypeAsync(any(), eq(msg.getOriginator()), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + .thenReturn(Futures.immediateFuture(Collections.singletonList(relation))); + when(ctx.getRelationService().deleteRelationAsync(any(), eq(relation))).thenReturn(Futures.immediateFuture(true)); + when(ctx.getRelationService().checkRelation(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + .thenReturn(Futures.immediateFuture(false)); + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + .thenReturn(Futures.immediateFuture(true)); + + node.onMsg(ctx, msg); + verify(ctx).tellNext(msg, TbRelationTypes.SUCCESS); + } + + @Test + public void testCreateNewRelationAndChangeOriginator() throws TbNodeException { + init(createRelationNodeConfigWithChangeOriginator()); + + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + + AssetId assetId = new AssetId(Uuids.timeBased()); + Asset asset = new Asset(); + asset.setId(assetId); + + when(assetService.findAssetByTenantIdAndName(any(), eq("AssetName"))).thenReturn(asset); + when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(asset)); + + TbMsgMetaData metaData = new TbMsgMetaData(); + metaData.putValue("name", "AssetName"); + metaData.putValue("type", "AssetType"); + msg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + + when(ctx.getRelationService().checkRelation(any(), eq(assetId), eq(deviceId), eq(RELATION_TYPE_CONTAINS), eq(RelationTypeGroup.COMMON))) + .thenReturn(Futures.immediateFuture(false)); + when(ctx.getRelationService().saveRelationAsync(any(), eq(new EntityRelation(assetId, deviceId, RELATION_TYPE_CONTAINS, RelationTypeGroup.COMMON)))) + .thenReturn(Futures.immediateFuture(true)); + + node.onMsg(ctx, msg); + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + assertEquals(assetId, originatorCaptor.getValue()); + } + + public void init(TbCreateRelationNodeConfiguration configuration) throws TbNodeException { + ObjectMapper mapper = new ObjectMapper(); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(configuration)); + + when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); + when(ctx.getRelationService()).thenReturn(relationService); + when(ctx.getAssetService()).thenReturn(assetService); + + node = new TbCreateRelationNode(); + node.init(ctx, nodeConfiguration); + } + + private TbCreateRelationNodeConfiguration createRelationNodeConfig() { + TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration(); + configuration.setDirection(EntitySearchDirection.FROM.name()); + configuration.setRelationType(RELATION_TYPE_CONTAINS); + configuration.setEntityCacheExpiration(300); + configuration.setEntityType("ASSET"); + configuration.setEntityNamePattern("${name}"); + configuration.setEntityTypePattern("${type}"); + configuration.setCreateEntityIfNotExists(false); + configuration.setChangeOriginatorToRelatedEntity(false); + configuration.setRemoveCurrentRelations(false); + return configuration; + } + + private TbCreateRelationNodeConfiguration createRelationNodeConfigWithRemoveCurrentRelations() { + TbCreateRelationNodeConfiguration configuration = createRelationNodeConfig(); + configuration.setRemoveCurrentRelations(true); + return configuration; + } + + private TbCreateRelationNodeConfiguration createRelationNodeConfigWithChangeOriginator() { + TbCreateRelationNodeConfiguration configuration = createRelationNodeConfig(); + configuration.setChangeOriginatorToRelatedEntity(true); + return configuration; + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java new file mode 100644 index 0000000000..a7b82fcf0c --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java @@ -0,0 +1,223 @@ +/** + * 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 static org.junit.Assert.*; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.http.Header; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.config.SocketConfig; +import org.apache.http.impl.bootstrap.HttpServer; +import org.apache.http.impl.bootstrap.ServerBootstrap; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpRequestHandler; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbEmail; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; +import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.ObjectMapper; + +@RunWith(MockitoJUnitRunner.class) +public class TbRestApiCallNodeTest { + + private TbRestApiCallNode restNode; + + @Mock + private TbContext ctx; + + private EntityId originator = new DeviceId(Uuids.timeBased()); + private TbMsgMetaData metaData = new TbMsgMetaData(); + + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + + private HttpServer server; + + public void setupServer(String pattern, HttpRequestHandler handler) throws IOException { + SocketConfig config = SocketConfig.custom().setSoReuseAddress(true).setTcpNoDelay(true).build(); + server = ServerBootstrap.bootstrap() + .setSocketConfig(config) + .registerHandler(pattern, handler) + .create(); + server.start(); + } + + private void initWithConfig(TbRestApiCallNodeConfiguration config) { + try { + ObjectMapper mapper = new ObjectMapper(); + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); + restNode = new TbRestApiCallNode(); + restNode.init(ctx, nodeConfiguration); + } catch (TbNodeException ex) { + throw new IllegalStateException(ex); + } + } + + @After + public void teardown() { + server.stop(); + } + + @Test + public void deleteRequestWithoutBody() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/path/to/delete"; + setupServer("*", new HttpRequestHandler() { + + @Override + public void handle(HttpRequest request, HttpResponse response, HttpContext context) + throws HttpException, IOException { + try { + assertEquals("Request path matches", request.getRequestLine().getUri(), path); + assertFalse("Content-Type not included", request.containsHeader("Content-Type")); + assertTrue("Custom header included", request.containsHeader("Foo")); + assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue()); + response.setStatusCode(200); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + // ignore + } finally { + latch.countDown(); + } + } + }).start(); + } catch ( Exception e ) { + System.out.println("Exception handling request: " + e.toString()); + e.printStackTrace(); + latch.countDown(); + } + } + }); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("DELETE"); + config.setHeaders(Collections.singletonMap("Foo", "Bar")); + config.setIgnoreRequestBody(true); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + restNode.onMsg(ctx, msg); + + assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS)); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + + assertEquals("USER", typeCaptor.getValue()); + assertEquals(originator, originatorCaptor.getValue()); + assertNotSame(metaData, metadataCaptor.getValue()); + assertEquals("{}", dataCaptor.getValue()); + } + + @Test + public void deleteRequestWithBody() throws IOException, InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final String path = "/path/to/delete"; + setupServer("*", new HttpRequestHandler() { + + @Override + public void handle(HttpRequest request, HttpResponse response, HttpContext context) + throws HttpException, IOException { + try { + assertEquals("Request path matches", path, request.getRequestLine().getUri()); + assertTrue("Content-Type included", request.containsHeader("Content-Type")); + assertEquals("Content-Type value", "text/plain;charset=ISO-8859-1", + request.getFirstHeader("Content-Type").getValue()); + assertTrue("Content-Length included", request.containsHeader("Content-Length")); + assertEquals("Content-Length value", "2", + request.getFirstHeader("Content-Length").getValue()); + assertTrue("Custom header included", request.containsHeader("Foo")); + assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue()); + response.setStatusCode(200); + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + // ignore + } finally { + latch.countDown(); + } + } + }).start(); + } catch ( Exception e ) { + System.out.println("Exception handling request: " + e.toString()); + e.printStackTrace(); + latch.countDown(); + } + } + }); + + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration(); + config.setRequestMethod("DELETE"); + config.setHeaders(Collections.singletonMap("Foo", "Bar")); + config.setIgnoreRequestBody(false); + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path)); + initWithConfig(config); + + TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId); + restNode.onMsg(ctx, msg); + + assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS)); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); + ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(String.class); + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture()); + + assertEquals("USER", typeCaptor.getValue()); + assertEquals(originator, originatorCaptor.getValue()); + assertNotSame(metaData, metadataCaptor.getValue()); + assertEquals("{}", dataCaptor.getValue()); + } + +} diff --git a/tools/pom.xml b/tools/pom.xml index 51c4dd689b..b68e4b94a3 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index cf54bcb496..cb0bb5e5ee 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 9aab46b824..e6b373b787 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -98,17 +98,33 @@ transport: bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}" # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" - # Path to the key store that holds the certificate - key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}" - # Password used to access the key store - key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" - # Password used to access the key - key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" - # Key alias - key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" - # Skip certificate validity check for client certificates. - skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" + # Server DTLS credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}" + # Password used to access the key store + store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}" + # Password used to access the key + key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}" x509: + # Skip certificate validity check for client certificates. + skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}" dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}" sessions: diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 4dd04c1932..7b92d5135f 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index f35f4136e7..20f18ccfda 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -19,6 +19,38 @@ server: address: "${HTTP_BIND_ADDRESS:0.0.0.0}" # Server bind port port: "${HTTP_BIND_PORT:8081}" + # Server SSL configuration + ssl: + # Enable/disable SSL support + enabled: "${SSL_ENABLED:false}" + # Server SSL credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${SSL_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${SSL_PEM_CERT:server.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${SSL_PEM_KEY:server_key.pem}" + # Server certificate private key password (optional) + key_password: "${SSL_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${SSL_KEY_STORE_TYPE:PKCS12}" + # Path to the key store that holds the SSL certificate + store_file: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}" + # Password used to access the key store + store_password: "${SSL_KEY_STORE_PASSWORD:thingsboard}" + # Key alias + key_alias: "${SSL_KEY_ALIAS:tomcat}" + # Password used to access the key + key_password: "${SSL_KEY_PASSWORD:thingsboard}" + # HTTP/2 support (takes effect only if server SSL is enabled) + http2: + # Enable/disable HTTP/2 support + enabled: "${HTTP2_ENABLED:true}" # Zookeeper connection parameters. Used for service discovery. zk: @@ -283,4 +315,4 @@ management: web: exposure: # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). - include: '${METRICS_ENDPOINTS_EXPOSE:info}' \ No newline at end of file + include: '${METRICS_ENDPOINTS_EXPOSE:info}' diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 4730be97bd..1b7ed8f6ca 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/src/main/data/lwm2mserver.jks b/transport/lwm2m/src/main/data/lwm2mserver.jks index 9f6748f8fd..5fab824aa1 100644 Binary files a/transport/lwm2m/src/main/data/lwm2mserver.jks and b/transport/lwm2m/src/main/data/lwm2mserver.jks differ diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 4280ccb1af..b5a6b2075c 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -111,9 +111,33 @@ transport: security: bind_address: "${LWM2M_SECURITY_BIND_ADDRESS:0.0.0.0}" bind_port: "${LWM2M_SECURITY_BIND_PORT:5686}" + # Server X509 Certificates support + credentials: + # Whether to enable LWM2M server X509 Certificate/RPK support + enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:true}" + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${LWM2M_SERVER_PEM_CERT:lwm2mserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${LWM2M_SERVER_PEM_KEY:lwm2mserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${LWM2M_SERVER_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${LWM2M_SERVER_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${LWM2M_SERVER_KEY_STORE:lwm2mserver.jks}" + # Password used to access the key store + store_password: "${LWM2M_SERVER_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}" + # Password used to access the key + key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_ks_password}" # Only Certificate_x509: - key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}" - key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_ks_password}" skip_validity_check_for_client_cert: "${TB_LWM2M_SERVER_SECURITY_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" bootstrap: enable: "${LWM2M_ENABLED_BS:true}" @@ -123,18 +147,51 @@ transport: security: bind_address: "${LWM2M_BS_SECURITY_BIND_ADDRESS:0.0.0.0}" bind_port: "${LWM2M_BS_SECURITY_BIND_PORT:5688}" - # Only Certificate_x509: - key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}" - key_password: "${LWM2M_BS_KEY_PASSWORD:server_ks_password}" + # Bootstrap server X509 Certificates support + credentials: + # Whether to enable LWM2M bootstrap server X509 Certificate/RPK support + enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:true}" + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${LWM2M_BS_PEM_CERT:lwm2mserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${LWM2M_BS_PEM_KEY:lwm2mserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${LWM2M_BS_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${LWM2M_BS_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${LWM2M_BS_KEY_STORE:lwm2mserver.jks}" + # Password used to access the key store + store_password: "${LWM2M_BS_KEY_STORE_PASSWORD:server_ks_password}" + # Key alias + key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}" + # Password used to access the key + key_password: "${LWM2M_BS_KEY_PASSWORD:server_ks_password}" security: - # Certificate_x509: - # To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format - # Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh - key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}" - # key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks" - key_store: "${LWM2M_KEYSTORE:lwm2mserver.jks}" - key_store_password: "${LWM2M_KEYSTORE_PASSWORD:server_ks_password}" - root_alias: "${LWM2M_SERVER_ROOT_CA_ALIAS:rootca}" + # X509 trust certificates + trust-credentials: + # Whether to load X509 trust certificates + enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:true}" + # Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore) + type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}" + # PEM certificates + pem: + # Path to the certificates file (holds trust certificates) + cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}" + # Keystore with trust certificates + keystore: + # Type of the key store + type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the X509 certificates + store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}" + # Password used to access the key store + store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}" timeout: "${LWM2M_TIMEOUT:120000}" @@ -142,11 +199,13 @@ transport: downlink_pool_size: "${LWM2M_DOWNLINK_POOL_SIZE:10}" ota_pool_size: "${LWM2M_OTA_POOL_SIZE:10}" clean_period_in_sec: "${LWM2M_CLEAN_PERIOD_IN_SEC:2}" - log_max_length: "${LWM2M_LOG_MAX_LENGTH:1024}" psm_activity_timer: "${LWM2M_PSM_ACTIVITY_TIMER:10000}" paging_transmission_window: "${LWM2M_PAGING_TRANSMISSION_WINDOW:10000}" # Use redis for Security and Registration stores redis.enabled: "${LWM2M_REDIS_ENABLED:false}" + network_config: # In this section you can specify custom parameters for LwM2M network configuration and expose the env variables to configure outside + # - key: "PROTOCOL_STAGE_THREAD_COUNT" + # value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}" stats: enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}" diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index dc2310f62e..1eac98020b 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index e0d0144d84..435b780818 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -104,16 +104,32 @@ transport: bind_address: "${MQTT_SSL_BIND_ADDRESS:0.0.0.0}" # MQTT SSL bind port bind_port: "${MQTT_SSL_BIND_PORT:8883}" - # SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext + # SSL protocol: See https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}" - # Path to the key store that holds the SSL certificate - key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}" - # Password used to access the key store - key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}" - # Password used to access the key - key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" - # Type of the key store - key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}" + # Server SSL credentials + credentials: + # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) + type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}" + # PEM server credentials + pem: + # Path to the server certificate file (holds server certificate or certificate chain, may include server private key) + cert_file: "${MQTT_SSL_PEM_CERT:mqttserver.pem}" + # Path to the server certificate private key file. Optional by default. Required if the private key is not present in server certificate file; + key_file: "${MQTT_SSL_PEM_KEY:mqttserver_key.pem}" + # Server certificate private key password (optional) + key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}" + # Keystore server credentials + keystore: + # Type of the key store + type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}" + # Path to the key store that holds the SSL certificate + store_file: "${MQTT_SSL_KEY_STORE:mqttserver.jks}" + # Password used to access the key store + store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}" + # Optional alias of the private key; If not set, the platform will load the first private key from the keystore; + key_alias: "${MQTT_SSL_KEY_ALIAS:}" + # Password used to access the key + key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}" # Skip certificate validity check for client certificates. skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}" sessions: diff --git a/transport/pom.xml b/transport/pom.xml index 9347afe94f..8b19669740 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 72201e25dd..520c651c32 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT transport diff --git a/ui-ngx/.browserslistrc b/ui-ngx/.browserslistrc new file mode 100644 index 0000000000..4f9ac26980 --- /dev/null +++ b/ui-ngx/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 5c24a0bfe3..7565ae770c 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -11,6 +11,9 @@ "schematics": { "@schematics/angular:component": { "style": "scss" + }, + "@schematics/angular:application": { + "strict": true } }, "architect": { @@ -79,6 +82,7 @@ "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/@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css", "node_modules/prismjs/themes/prism.css", "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css" ], @@ -91,13 +95,15 @@ "node_modules/tinycolor2/dist/tinycolor-min.js", "node_modules/split.js/dist/split.min.js", "node_modules/systemjs/dist/system.js", + "node_modules/systemjs-babel/dist/systemjs-babel.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-javascript.min.js", - "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js" + "node_modules/prismjs/components/prism-typescript.min.js", + "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.min.js" ], "customWebpackConfig": { "path": "./extra-webpack.config.js" @@ -128,7 +134,9 @@ "jquery.terminal", "tooltipster", "jstree", - "qrcode" + "qrcode", + "wcwidth", + "leaflet-polylinedecorator" ] }, "configurations": { @@ -139,7 +147,14 @@ "with": "src/environments/environment.prod.ts" } ], - "optimization": true, + "optimization": { + "scripts": true, + "styles": { + "minify": true, + "inlineCritical": false + }, + "fonts": false + }, "outputHashing": "all", "sourceMap": false, "namedChunks": false, @@ -153,6 +168,21 @@ "maximumError": "12mb" } ] + }, + "development": { + "buildOptimizer": false, + "optimization": { + "scripts": false, + "styles": { + "minify": false, + "inlineCritical": false + }, + "fonts": false + }, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true } } }, @@ -165,8 +195,12 @@ "configurations": { "production": { "browserTarget": "thingsboard:build:production" + }, + "development": { + "browserTarget": "thingsboard:build:development" } - } + }, + "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", diff --git a/ui-ngx/extra-webpack.config.js b/ui-ngx/extra-webpack.config.js index ef2e115252..e1efad5e67 100644 --- a/ui-ngx/extra-webpack.config.js +++ b/ui-ngx/extra-webpack.config.js @@ -14,7 +14,7 @@ * limitations under the License. */ const CompressionPlugin = require("compression-webpack-plugin"); -const TerserPlugin = require("terser-webpack-plugin"); +const JavaScriptOptimizerPlugin = require("@angular-devkit/build-angular/src/webpack/plugins/javascript-optimizer-plugin").JavaScriptOptimizerPlugin; const webpack = require("webpack"); const dirTree = require("directory-tree"); const ngWebpack = require('@ngtools/webpack'); @@ -52,7 +52,10 @@ module.exports = (config, options) => { }) ); config.plugins.push( - new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) + new webpack.IgnorePlugin({ + resourceRegExp: /^\.\/locale$/, + contextRegExp: /moment$/, + }) ); if (config.mode === 'production') { @@ -62,11 +65,10 @@ module.exports = (config, options) => { angularCompilerOptions.emitNgModuleScope = true; config.plugins.splice(index, 1); config.plugins.push(new ngWebpack.ivy.AngularWebpackPlugin(angularCompilerOptions)); - const terserPluginOptions = config.optimization.minimizer[1].options; - delete terserPluginOptions.terserOptions.compress.global_defs.ngJitMode; - terserPluginOptions.terserOptions.compress.side_effects = false; + const javascriptOptimizerOptions = config.optimization.minimizer[1].options; + delete javascriptOptimizerOptions.define.ngJitMode; config.optimization.minimizer.splice(1, 1); - config.optimization.minimizer.push(new TerserPlugin(terserPluginOptions)); + config.optimization.minimizer.push(new JavaScriptOptimizerPlugin(javascriptOptimizerOptions)); } return config; }; diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 2aaaff7b66..adec5e815a 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,11 +1,11 @@ { "name": "thingsboard", - "version": "3.3.2", + "version": "3.3.3", "scripts": { "ng": "ng", - "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --host 0.0.0.0 --open", + "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", "build": "ng build", - "build:prod": "ng build --prod --vendor-chunk", + "build:prod": "ng build --configuration production --vendor-chunk", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", @@ -13,135 +13,141 @@ }, "private": true, "dependencies": { - "@angular/animations": "^11.2.14", - "@angular/cdk": "^11.2.13", - "@angular/common": "^11.2.14", - "@angular/compiler": "^11.2.14", - "@angular/core": "^11.2.14", - "@angular/flex-layout": "^11.0.0-beta.33", - "@angular/forms": "^11.2.14", - "@angular/material": "^11.2.13", - "@angular/platform-browser": "^11.2.14", - "@angular/platform-browser-dynamic": "^11.2.14", - "@angular/router": "^11.2.14", + "@angular/animations": "^12.2.13", + "@angular/cdk": "^12.2.13", + "@angular/common": "^12.2.13", + "@angular/compiler": "^12.2.13", + "@angular/core": "^12.2.13", + "@angular/flex-layout": "^12.0.0-beta.35", + "@angular/forms": "^12.2.13", + "@angular/material": "^12.2.13", + "@angular/platform-browser": "^12.2.13", + "@angular/platform-browser-dynamic": "^12.2.13", + "@angular/router": "^12.2.13", "@auth0/angular-jwt": "^5.0.2", - "@date-io/date-fns": "^2.10.11", + "@date-io/date-fns": "^2.11.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "~0.4.6", + "@geoman-io/leaflet-geoman-free": "^2.11.3", "@juggle/resize-observer": "^3.3.1", - "@mat-datetimepicker/core": "~6.0.2", - "@material-ui/core": "^4.11.4", + "@mat-datetimepicker/core": "~7.0.1", + "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/pickers": "^3.3.10", - "@ngrx/effects": "^10.1.2", - "@ngrx/store": "^10.1.2", - "@ngrx/store-devtools": "^10.1.2", + "@ngrx/effects": "^12.5.1", + "@ngrx/store": "^12.5.1", + "@ngrx/store-devtools": "^12.5.1", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", - "ace-builds": "^1.4.12", - "angular-gridster2": "~11.2.0", - "angular2-hotkeys": "^2.2.0", + "ace-builds": "^1.4.13", + "angular-gridster2": "~12.1.1", + "angular2-hotkeys": "^2.4.0", "canvas-gauges": "^2.1.7", - "compass-sass-mixins": "^0.12.7", - "core-js": "^3.12.1", - "date-fns": "^2.21.3", + "core-js": "^3.19.2", + "date-fns": "^2.26.0", "flot": "git://github.com/thingsboard/flot.git#0.9-work", "flot.curvedlines": "git://github.com/MichaelZinsmaier/CurvedLines.git#master", "font-awesome": "^4.7.0", - "html2canvas": "^1.0.0-rc.7", - "jquery": "^3.5.1", - "jquery.terminal": "^2.24.0", - "js-beautify": "^1.13.13", + "html2canvas": "^1.3.3", + "jquery": "^3.6.0", + "jquery.terminal": "^2.29.4", + "js-beautify": "^1.14.0", "json-schema-defaults": "^0.4.0", - "jstree": "^3.3.11", + "jstree": "^3.3.12", "jstree-bootstrap-theme": "^1.0.1", - "jszip": "^3.6.0", + "jszip": "^3.7.1", "leaflet": "^1.7.1", - "leaflet-editable": "^1.2.0", "leaflet-polylinedecorator": "^1.6.0", - "leaflet-providers": "^1.12.0", - "leaflet.gridlayer.googlemutant": "0.10.2", - "leaflet.markercluster": "^1.5.0", + "leaflet-providers": "^1.13.0", + "leaflet.gridlayer.googlemutant": "^0.13.4", + "leaflet.markercluster": "^1.5.3", "material-design-icons": "^3.0.1", "messageformat": "^2.3.0", "moment": "^2.29.1", - "moment-timezone": "^0.5.33", - "mousetrap": "1.6.3", - "ngx-clipboard": "^14.0.1", + "moment-timezone": "^0.5.34", + "ngx-clipboard": "^14.0.2", "ngx-color-picker": "^11.0.0", - "ngx-daterangepicker-material": "^4.0.1", + "ngx-daterangepicker-material": "^5.0.1", "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": "^11.1.3", - "ngx-sharebuttons": "^8.0.5", - "ngx-translate-messageformat-compiler": "^4.9.0", + "ngx-flowchart": "git://github.com/thingsboard/ngx-flowchart.git#release/1.0.0", + "ngx-hm-carousel": "^2.0.1", + "ngx-markdown": "^12.0.1", + "ngx-sharebuttons": "^9.0.0", + "ngx-translate-messageformat-compiler": "^4.11.0", "objectpath": "^2.0.0", - "prettier": "^2.1.2", + "prettier": "^2.5.0", "prop-types": "^15.7.2", - "qrcode": "^1.4.4", + "qrcode": "^1.5.0", "raphael": "^2.3.0", - "rc-select": "~10.5.1", - "react": "~16.14.0", - "react-ace": "^9.1.4", - "react-dom": "^16.13.1", - "react-dropzone": "^11.2.0", + "rc-select": "~13.2.0", + "react": "~17.0.2", + "react-ace": "^9.5.0", + "react-dom": "^17.0.2", + "react-dropzone": "^11.4.2", "reactcss": "^1.2.3", "rxjs": "~6.6.7", "schema-inspector": "^2.0.1", - "screenfull": "^5.1.0", + "screenfull": "^6.0.0", "split.js": "^1.6.4", - "systemjs": "0.21.5", + "systemjs": "6.11.0", + "systemjs-babel": "^0.3.1", "tinycolor2": "^1.4.2", "tooltipster": "^4.2.8", - "tslib": "^2.2.0", + "tslib": "^2.3.1", "tv4": "^1.3.0", "typeface-roboto": "^1.1.13", "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-builders/custom-webpack": "~11.1.1", - "@angular-devkit/build-angular": "^0.1102.13", - "@angular/cli": "^11.2.13", - "@angular/compiler-cli": "^11.2.14", - "@angular/language-service": "^11.2.14", - "@ngtools/webpack": "~11.2.13", - "@types/canvas-gauges": "^2.1.2", - "@types/flot": "^0.0.31", - "@types/jasmine": "~3.7.4", - "@types/jasminewd2": "^2.0.9", - "@types/jquery": "^3.5.2", - "@types/js-beautify": "^1.13.1", - "@types/jstree": "^3.3.40", - "@types/leaflet": "1.5.17", - "@types/leaflet-editable": "^1.2.1", - "@types/leaflet-polylinedecorator": "^1.6.0", - "@types/leaflet.markercluster": "^1.4.4", - "@types/lodash": "^4.14.170", + "@angular-builders/custom-webpack": "~12.1.3", + "@angular-devkit/build-angular": "^12.2.13", + "@angular/cli": "^12.2.13", + "@angular/compiler-cli": "^12.2.13", + "@angular/language-service": "^12.2.13", + "@ngtools/webpack": "~12.2.13", + "@types/canvas-gauges": "^2.1.4", + "@types/flot": "^0.0.32", + "@types/jasmine": "~3.10.2", + "@types/jasminewd2": "^2.0.10", + "@types/jquery": "^3.5.9", + "@types/js-beautify": "^1.13.3", + "@types/jstree": "^3.3.41", + "@types/leaflet": "^1.7.6", + "@types/leaflet-polylinedecorator": "^1.6.1", + "@types/leaflet-providers": "^1.2.1", + "@types/leaflet.gridlayer.googlemutant": "^0.4.6", + "@types/leaflet.markercluster": "^1.4.6", + "@types/lodash": "^4.14.177", "@types/moment-timezone": "^0.5.30", - "@types/mousetrap": "1.6.3", - "@types/raphael": "^2.3.1", - "@types/react": "^16.9.51", - "@types/react-dom": "^16.9.8", - "@types/tinycolor2": "^1.4.2", - "@types/tooltipster": "^0.0.30", + "@types/mousetrap": "^1.6.0", + "@types/node": "~15.14.9", + "@types/raphael": "^2.3.2", + "@types/react": "^17.0.37", + "@types/react-dom": "^17.0.11", + "@types/systemjs": "6.1.1", + "@types/tinycolor2": "^1.4.3", + "@types/tooltipster": "^0.0.31", "codelyzer": "^6.0.2", - "compression-webpack-plugin": "^6.1.1", - "directory-tree": "^2.2.4", - "jasmine-core": "~3.7.1", + "compression-webpack-plugin": "^9.0.1", + "directory-tree": "^3.0.1", + "jasmine-core": "~3.10.1", "jasmine-spec-reporter": "~7.0.0", - "karma": "~6.3.2", + "karma": "~6.3.9", "karma-chrome-launcher": "~3.1.0", "karma-coverage-istanbul-reporter": "~3.0.3", - "karma-jasmine": "~4.0.0", - "karma-jasmine-html-reporter": "^1.5.0", + "karma-jasmine": "~4.0.1", + "karma-jasmine-html-reporter": "^1.7.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", + "raw-loader": "^4.0.2", + "ts-node": "^10.4.0", "tslint": "~6.1.3", - "typescript": "~4.0.3", - "webpack": "^4.46.0" + "typescript": "~4.3.5", + "webpack": "^5.64.4" + }, + "resolutions": { + "lodash": "~4.17.21" } } diff --git a/ui-ngx/patches/canvas-gauges+2.1.7.patch b/ui-ngx/patches/canvas-gauges+2.1.7.patch new file mode 100644 index 0000000000..7f83155d41 --- /dev/null +++ b/ui-ngx/patches/canvas-gauges+2.1.7.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/canvas-gauges/gauge.min.js b/node_modules/canvas-gauges/gauge.min.js +index 6aea775..d9fe5d3 100644 +--- a/node_modules/canvas-gauges/gauge.min.js ++++ b/node_modules/canvas-gauges/gauge.min.js +@@ -23,5 +23,5 @@ + * + * @version 2.1.7 + */ +-!function(e){"use strict";function t(e){if(Array.isArray(e)){for(var t=0,i=Array(e.length);t1&&(d=1),1!==d&&(c=r(d),isFinite(c)&&!isNaN(c)&&(d=c)),t&&t(d),s0){for(a=e.toFixed(i).toString().split("."),n=r-a[0].length;o1?(r=~i.indexOf("."),~i.indexOf("-")?"-"+[t.majorTicksInt+t.majorTicksDec+2+(r?1:0)-i.length].join("0")+i.replace("-",""):[t.majorTicksInt+t.majorTicksDec+1+(r?1:0)-i.length].join("0")+i):i}function m(e){return e*Math.PI/180}function v(e,t){return{x:-e*Math.sin(t),y:e*Math.cos(t)}}function g(e,t,i,r){var o=!(arguments.length>4&&void 0!==arguments[4])||arguments[4],n=arguments.length>5&&void 0!==arguments[5]?arguments[5]:0,a=e.createLinearGradient(o?0:n,o?n:0,o?0:r,o?r:0);return a.addColorStop(0,t),a.addColorStop(1,i),a}function b(e,t){if(arguments.length>2&&void 0!==arguments[2]&&arguments[2])return e.restore(),!0;e.save();var i=t.borderShadowWidth;return i&&(e.shadowBlur=i,e.shadowColor=t.colorBorderShadow),!0}function p(e,t){t.needleShadow&&(e.shadowOffsetX=2,e.shadowOffsetY=2,e.shadowBlur=10,e.shadowColor=t.colorNeedleShadowDown)}function w(e,t,i){return e["font"+t+"Style"]+" "+e["font"+t+"Weight"]+" "+e["font"+t+"Size"]*i+"px "+e["font"+t]}function k(e){e.shadowOffsetX=null,e.shadowOffsetY=null,e.shadowBlur=null,e.shadowColor="",e.strokeStyle=null,e.lineWidth=0,e.save()}function y(e,t,i,r){t.valueTextShadow&&(e.shadowOffsetX=i,e.shadowOffsetY=i,e.shadowBlur=r,e.shadowColor=t.colorValueTextShadow)}function x(e,t,i,r,o,n){if(t.valueBox){k(e);var a=t.valueDec?1+t.valueDec:0,l="9".repeat(Math.max.apply(null,[String(parseInt(i)).length+a].concat(t.majorTicks.map(function(e){return String(parseInt(e,10)).length+a})))),s=t.valueText||u(i,t),d=n/200,c=n/100,f=.4*c,m=1.2*c;e.font=w(t,"Value",d),y(e,t,f,m);var v=e.measureText(t.valueText?s:"-"+u(Number(l),t)).width;k(e);var g=parseFloat(t.fontValueSize)*d+f+m,b=c*parseFloat(t.valueBoxStroke),p=2*n-2*b,x=v+10*c,T=1.1*g+f+m,S=c*t.valueBoxBorderRadius,V=(parseFloat(t.valueBoxWidth)||0)/100*p;V>x&&(x=V),x>p&&(x=p);var W=r-x/2,O=o-T/2,A=o-5.75*c;if(e.beginPath(),S?h(e,W,O,x,T,S):e.rect(W,O,x,T),b){var P=e.createRadialGradient(r,A,10*c,r,A,20*c);P.addColorStop(0,t.colorValueBoxRect),P.addColorStop(1,t.colorValueBoxRectEnd),e.strokeStyle=P,e.lineWidth=b,e.stroke()}t.colorValueBoxShadow&&(e.shadowBlur=1.2*c,e.shadowColor=t.colorValueBoxShadow),t.colorValueBoxBackground&&(e.fillStyle=t.colorValueBoxBackground,e.fill()),e.closePath(),e.restore(),y(e,t,f,m),e.fillStyle=t.colorValueText,e.textAlign="center",e.textBaseline="alphabetic",e.fillText(s,W+x/2,o+T/2-g/3),e.restore()}}function T(e){var t=e.value,i=e.minValue,r=e.maxValue,o=.01*(r-i);return{normal:tr?r:t,indented:tr?r+o:t}}function S(e,t,i,r,o){i.beginPath(),i.arc(0,0,xe(e),0,2*Oe,!0),i.lineWidth=t,i.strokeStyle=o?We.linearGradient(i,r,o,e):r,i.stroke(),i.closePath()}function V(e,t){var i=pe.pixelRatio;return e.maxRadius||(e.maxRadius=e.max-t.borderShadowWidth-t.borderOuterWidth*i-t.borderMiddleWidth*i-t.borderInnerWidth*i+(t.borderOuterWidth?.5:0)+(t.borderMiddleWidth?.5:0)+(t.borderInnerWidth?.5:0)),e.maxRadius}function W(e,t){var i=pe.pixelRatio,r=t.borderShadowWidth*i,o=e.max-r-t.borderOuterWidth*i/2,n=o-t.borderOuterWidth*i/2-t.borderMiddleWidth*i/2+.5,a=n-t.borderMiddleWidth*i/2-t.borderInnerWidth*i/2+.5,l=V(e,t),s=void 0,d=!1;e.save(),t.borderOuterWidth&&(d=We.drawShadow(e,t,d),S(o,t.borderOuterWidth*i,e,t.colorBorderOuter,t.colorBorderOuterEnd)),t.borderMiddleWidth&&(d=We.drawShadow(e,t,d),S(n,t.borderMiddleWidth*i,e,t.colorBorderMiddle,t.colorBorderMiddleEnd)),t.borderInnerWidth&&(d=We.drawShadow(e,t,d),S(a,t.borderInnerWidth*i,e,t.colorBorderInner,t.colorBorderInnerEnd)),We.drawShadow(e,t,d),e.beginPath(),e.arc(0,0,xe(l),0,2*Oe,!0),t.colorPlateEnd?(s=e.createRadialGradient(0,0,l/2,0,0,l),s.addColorStop(0,t.colorPlate),s.addColorStop(1,t.colorPlateEnd)):s=t.colorPlate,e.fillStyle=s,e.fill(),e.closePath(),e.restore()}function O(e,t){var i=e.max*(parseFloat(t.highlightsWidth)||0)/100;if(i){var r=xe(P(e,t)-i/2),o=0,n=t.highlights.length,a=(t.maxValue-t.minValue)/t.ticksAngle;for(e.save();on?o:n,n>o,o>n?i:r):a,t>0?We.roundRect(e,i,r,o,n,t):e.rect(i,r,o,n),e.fill(),e.closePath()}function G(e,t,i,r,o,n,a,l,s){e.beginPath(),e.lineWidth=t,e.strokeStyle=s?We.linearGradient(e,l,s,a,!0,o):l,i>0?We.roundRect(e,r,o,n,a,i):e.rect(r,o,n,a),e.stroke(),e.closePath()}function F(e,t,i,r,o,n){var a=pe.pixelRatio;e.save();var l=t.borderRadius*a,s=o-t.borderShadowWidth-t.borderOuterWidth*a,d=s-t.borderOuterWidth*a-t.borderMiddleWidth*a,c=d-t.borderMiddleWidth*a-t.borderInnerWidth*a,h=c-t.borderInnerWidth*a,u=n-t.borderShadowWidth-t.borderOuterWidth*a,f=u-t.borderOuterWidth*a-t.borderMiddleWidth*a,m=f-t.borderMiddleWidth*a-t.borderInnerWidth*a,v=m-t.borderInnerWidth*a,g=i-(d-s)/2,b=g-(c-d)/2,p=b-(h-c)/2,w=r-(f-u)/2,k=w-(m-f)/2,y=k-(v-m)/2,x=0,T=!1;return t.borderOuterWidth&&(T=We.drawShadow(e,t,T),G(e,t.borderOuterWidth*a,l,i+t.borderOuterWidth*a/2-x,r+t.borderOuterWidth*a/2-x,s,u,t.colorBorderOuter,t.colorBorderOuterEnd),x+=.5*a),t.borderMiddleWidth&&(T=We.drawShadow(e,t,T),G(e,t.borderMiddleWidth*a,l-=1+2*x,g+t.borderMiddleWidth*a/2-x,w+t.borderMiddleWidth*a/2-x,d+2*x,f+2*x,t.colorBorderMiddle,t.colorBorderMiddleEnd),x+=.5*a),t.borderInnerWidth&&(T=We.drawShadow(e,t,T),G(e,t.borderInnerWidth*a,l-=1+2*x,b+t.borderInnerWidth*a/2-x,k+t.borderInnerWidth*a/2-x,c+2*x,m+2*x,t.colorBorderInner,t.colorBorderInnerEnd),x+=.5*a),We.drawShadow(e,t,T),L(e,l,p,y,h+2*x,v+2*x,t.colorPlate,t.colorPlateEnd),e.restore(),[p,y,h,v]}function X(e,t,i,r,o,n){var a=pe.pixelRatio,l=n>=o,s=l?.85*o:n,d=l?n:o;i=l?ye(i+(o-s)/2):i;var c=!!t.title,h=!!t.units,u=!!t.valueBox,f=void 0,m=void 0,v=void 0;l?(m=ye(.05*d),f=ye(.075*d),v=ye(.11*d),c&&(d-=f,r+=f),h&&(d-=m),u&&(d-=v)):(m=f=ye(.15*s),c&&(s-=f,r+=f),h&&(s-=m));var g=2*t.barStrokeWidth,b=t.barBeginCircle?ye(s*t.barBeginCircle/200-g/2):0,p=ye(s*t.barWidth/100-g),w=ye(d*t.barLength/100-g),k=ye((d-w)/2),y=ye(i+(l?s/2:k+b)),x=ye(r+(l?d-k-b+g/2:s/2)),T=!l||t.hasLeft&&t.hasRight?0:(t.hasRight?-1:1)*t.ticksWidth/100*s,S=l||t.hasLeft&&t.hasRight?0:(t.hasRight?-1:1)*t.ticksWidth/100*s;return e.barDimensions={isVertical:l,width:s,length:d,barWidth:p,barLength:w,strokeWidth:g,barMargin:k,radius:b,pixelRatio:a,barOffset:null,titleMargin:c?f:0,unitsMargin:h?m:0,get ticksLength(){return this.barLength-this.barOffset-this.strokeWidth},X:i+T,Y:r+S,x0:y+T,y0:x+S,baseX:i,baseY:r,ticksPadding:t.ticksPadding/100},e.barDimensions}function Y(e,t,i,r,o,n,a){var l=X(e,t,r,o,n,a),s=l.isVertical,d=l.width,c=l.barWidth,h=l.barLength,u=l.strokeWidth,f=l.barMargin,m=l.radius,v=l.x0,g=l.y0,b=l.X,p=l.Y,w=h;if(e.save(),e.beginPath(),t.barBeginCircle){var k=We.radians(s?270:0),y=Math.asin(c/2/m),x=Math.cos(y),T=Math.sin(y),S=v+(s?m*T:m*x-u/2),V=s?g-m*x:g+m*T,W=xe(s?V-g:S-v);e.barDimensions.barOffset=ye(W+m);var O=s?ye(v-m*T):S,A=s?V:ye(g-m*T);"progress"===i&&(h=e.barDimensions.barOffset+(h-e.barDimensions.barOffset)*(We.normalizedValue(t).normal-t.minValue)/(t.maxValue-t.minValue));var P=ye(S+h-e.barDimensions.barOffset+u/2),M=ye(V-h+e.barDimensions.barOffset-u/2);e.arc(v,g,m,k+y,k-y),s?(e.moveTo(S,A),e.lineTo(S,M),e.lineTo(O,M),e.lineTo(O,A)):(e.moveTo(S,A),e.lineTo(P,A),e.lineTo(P,V),e.lineTo(S,V))}else{var B=ye(s?b+(d-c)/2:b+f),C=ye(s?p+h+f:p+(d-c)/2);"progress"===i&&(h*=(t.value-t.minValue)/(t.maxValue-t.minValue)),s?e.rect(B,C,c,-h):e.rect(B,C,h,c)}"progress"!==i&&t.barStrokeWidth&&(e.lineWidth=u,e.strokeStyle=t.colorBarStroke,e.stroke()),"progress"!==i&&t.colorBar?(e.fillStyle=t.colorBarEnd?We.linearGradient(e,t.colorBar,t.colorBarEnd,h,s,s?p:b):t.colorBar,e.fill()):"progress"===i&&t.colorBarProgress&&(e.fillStyle=t.colorBarProgressEnd?We.linearGradient(e,t.colorBarProgress,t.colorBarProgressEnd,w,s,s?p:b):t.colorBarProgress,e.fill()),e.closePath(),t.barBeginCircle&&(e.barDimensions.radius+=u),e.barDimensions.barWidth+=u,e.barDimensions.barLength+=u}function U(e,t,i,r,o,n){Y(e,t,"",i,r,o,n)}function q(e,t){return t.needleSide!==e||t.tickSide!==e||t.numberSide!==e}function H(e,t,i,r,o,n){t.barProgress&&Y(e,t,"progress",i,r,o,n)}function J(e,t){var i=e.barDimensions,r=i.isVertical,o=i.width,n=i.length,a=i.barWidth,l=i.barOffset,s=i.barMargin,d=i.X,c=i.Y,h=i.ticksLength,u=i.ticksPadding,f=o*(parseFloat(t.highlightsWidth)||0)/100;if(t.highlights&&f){var m="right"!==t.tickSide,v="left"!==t.tickSide,g=0,b=t.highlights.length,p=(o-a)/2,w=t.maxValue-t.minValue,k=ye(r?d+p:d+s+l),y=f,x=r?c+n-s-l:c+p,T=ye((t.ticksWidth/100+u)*o)+(f-t.ticksWidth/100*o),S=ye(a+u*o);for(e.save();gn&&(d*=-1),e.moveTo(i-h,r),e.lineTo(i+h,r),e.lineTo(i+h,r+d),e.lineTo(i,n),e.lineTo(i-h,r+d),e.lineTo(i-h,r)):(i>o&&(d*=-1),e.moveTo(i,r-h),e.lineTo(i,r+h),e.lineTo(i+d,r+h),e.lineTo(o,r),e.lineTo(i+d,r-h),e.lineTo(i,r-h)),e.fill(),e.closePath()}function se(e,t,i,r,o,n,a){var l=(parseFloat(t.fontValueSize)||0)*n/200,s=(.11*a-l)/2;e.barDimensions.isVertical&&We.drawValueBox(e,t,i,r+n/2,o+a-l-s,n)}var de=function(){function e(e,t){var i=[],r=!0,o=!1,n=void 0;try{for(var a,l=e[Symbol.iterator]();!(r=(a=l.next()).done)&&(i.push(a.value),!t||i.length!==t);r=!0);}catch(e){o=!0,n=e}finally{try{!r&&l.return&&l.return()}finally{if(o)throw n}}return i}return function(t,i){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,i);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),ce=function e(t,i,r){null===t&&(t=Function.prototype);var o=Object.getOwnPropertyDescriptor(t,i);if(void 0===o){var n=Object.getPrototypeOf(t);return null===n?void 0:e(n,i,r)}if("value"in o)return o.value;var a=o.get;if(void 0!==a)return a.call(r)},he=function e(t,i,r,o){var n=Object.getOwnPropertyDescriptor(t,i);if(void 0===n){var a=Object.getPrototypeOf(t);null!==a&&e(a,i,r,o)}else if("value"in n&&n.writable)n.value=r;else{var l=n.set;void 0!==l&&l.call(o,r)}return r},ue=function(){function e(e,t){for(var i=0;i>>0;if(0===o)return-1;var n=+t||0;if(Math.abs(n)===1/0&&(n=0),n>=o)return-1;for(i=Math.max(n>=0?n:o-Math.abs(n),0);i>>0,r=arguments[1],o=r>>0,n=o<0?Math.max(i+o,0):Math.min(o,i),a=arguments[2],l=void 0===a?i:a>>0,s=l<0?Math.max(i+l,0):Math.min(l,i);n1?r-1:0),n=1;n1?t-1:0),r=1;r=(7-4*t)/11)return-Math.pow((11-6*t-11*e)/4,2)+Math.pow(i,2)},elastic:function(e){return 1-ve.delastic(1-e)},delastic:function(e){return Math.pow(2,10*(e-1))*Math.cos(20*Math.PI*1.5/3*e)}},ge=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"linear",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:250,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){};if(o(this,e),this.duration=i,this.rule=t,this.draw=r,this.end=n,"function"!=typeof this.draw)throw new TypeError("Invalid animation draw callback:",r);if("function"!=typeof this.end)throw new TypeError("Invalid animation end callback:",n)}return ue(e,[{key:"animate",value:function(e,t){var i=this;this.frame&&this.cancel();var r=window.performance&&window.performance.now?window.performance.now():n("animationStartTime")||Date.now();e=e||this.draw,t=t||this.end,this.draw=e,this.end=t,this.frame=me(function(o){return a(o,e,r,ve[i.rule]||i.rule,i.duration,t,i)})}},{key:"cancel",value:function(){if(this.frame){(n("cancelAnimationFrame")||function(e){})(this.frame),this.frame=null}}},{key:"destroy",value:function(){this.cancel(),this.draw=null,this.end=null}}]),e}();ge.rules=ve;var be=function(){function t(i,r,n){o(this,t),this.options=i,this.element=r.toLowerCase(),this.type=t.toDashed(n),this.Type=e[n],this.mutationsObserved=!1,this.isObservable=!!window.MutationObserver,window.GAUGES_NO_AUTO_INIT||t.domReady(this.traverse.bind(this))}return ue(t,[{key:"isValidNode",value:function(e){return!(!e.tagName||e.tagName.toLowerCase()!==this.element||e.getAttribute("data-type")!==this.type)}},{key:"traverse",value:function(){for(var e=document.getElementsByTagName(this.element),t=0,i=e.length;t1&&void 0!==arguments[1])||arguments[1],i=e.split(/-/),r=0,o=i.length,n="";r1&&void 0!==arguments[1]?arguments[1]:0;return e=parseFloat(e),!isNaN(e)&&isFinite(e)||(e=parseFloat(t)||0),e}},{key:"mod",value:function(e,t){return(e%t+t)%t}},{key:"version",get:function(){return ke}}]),n}(fe);void 0!==e&&(e.BaseGauge=Se,e.gauges=(window.document||{}).gauges=Te);var Ve=/{([_a-zA-Z]+[_a-zA-Z0-9]*)}/g,We={roundRect:h,padValue:u,formatMajorTickNumber:f,radians:m,radialPoint:v,linearGradient:g,drawNeedleShadow:p,drawValueBox:x,verifyError:s,prepareTicks:c,drawShadow:b,font:w,normalizedValue:T,formatContext:d},Oe=Math.PI,Ae=Oe/2,Pe=Object.assign({},we,{ticksAngle:270,startAngle:45,colorNeedleCircleOuter:"#f0f0f0",colorNeedleCircleOuterEnd:"#ccc",colorNeedleCircleInner:"#e8e8e8",colorNeedleCircleInnerEnd:"#f5f5f5",needleCircleSize:10,needleCircleInner:!0,needleCircleOuter:!0,needleStart:20,animationTarget:"needle",useMinPath:!1,barWidth:0,barStartPosition:"left"}),Me=function(e){function t(e){return o(this,t),e=Object.assign({},Pe,e||{}),i(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,t.configure(e)))}return r(t,e),ue(t,[{key:"draw",value:function(){try{var e=this.canvas,i=[-e.drawX,-e.drawY,e.drawWidth,e.drawHeight],r=i[0],o=i[1],n=i[2],a=i[3],l=this.options;if("needle"===l.animationTarget){if(!e.elementClone.initialized){var s=e.contextClone;s.clearRect(r,o,n,a),s.save(),this.emit("beforePlate"),W(s,l),this.emit("beforeHighlights"),O(s,l),this.emit("beforeMinorTicks"),A(s,l),this.emit("beforeMajorTicks"),M(s,l),this.emit("beforeNumbers"),j(s,l),this.emit("beforeTitle"),N(s,l),this.emit("beforeUnits"),E(s,l),e.elementClone.initialized=!0}this.canvas.commit(),e.context.clearRect(r,o,n,a),e.context.save(),e.context.drawImage(e.elementClone,r,o,n,a),e.context.save(),this.emit("beforeProgressBar"),D(e.context,l),this.emit("beforeValueBox"),R(e.context,l,z(this)),this.emit("beforeNeedle"),_(e.context,l)}else{var d=-We.radians((l.value-l.minValue)/(l.maxValue-l.minValue)*l.ticksAngle);if(e.context.clearRect(r,o,n,a),e.context.save(),this.emit("beforePlate"),W(e.context,l),e.context.rotate(d),this.emit("beforeHighlights"),O(e.context,l),this.emit("beforeMinorTicks"),A(e.context,l),this.emit("beforeMajorTicks"),M(e.context,l),this.emit("beforeNumbers"),j(e.context,l),this.emit("beforeProgressBar"),D(e.context,l),e.context.rotate(-d),e.context.save(),!e.elementClone.initialized){var c=e.contextClone;c.clearRect(r,o,n,a),c.save(),this.emit("beforeTitle"),N(c,l),this.emit("beforeUnits"),E(c,l),this.emit("beforeNeedle"),_(c,l),e.elementClone.initialized=!0}e.context.drawImage(e.elementClone,r,o,n,a)}this.emit("beforeValueBox"),R(e.context,l,z(this)),ce(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"draw",this).call(this)}catch(e){We.verifyError(e)}return this}},{key:"value",set:function(e){e=Se.ensureValue(e,this.options.minValue),this.options.animation&&360===this.options.ticksAngle&&this.options.useMinPath&&(this._value=e,e=this.options.value+((e-this.options.value)%360+540)%360-180),he(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"value",e,this)},get:function(){return ce(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"value",this)}}],[{key:"configure",value:function(e){return e.barWidth>50&&(e.barWidth=50),isNaN(e.startAngle)&&(e.startAngle=45),isNaN(e.ticksAngle)&&(e.ticksAngle=270),e.ticksAngle>360&&(e.ticksAngle=360),e.ticksAngle<0&&(e.ticksAngle=0),e.startAngle<0&&(e.startAngle=0),e.startAngle>360&&(e.startAngle=360),e}}]),t}(Se);void 0!==e&&(e.RadialGauge=Me),Se.initialize("RadialGauge",Pe);var Be=Object.assign({},we,{borderRadius:0,barBeginCircle:30,colorBarEnd:"",colorBarProgressEnd:"",needleWidth:6,tickSide:"both",needleSide:"both",numberSide:"both",ticksWidth:10,ticksWidthMinor:5,ticksPadding:5,barLength:85,fontTitleSize:26,highlightsWidth:10}),Ce=function(e){function n(e){return o(this,n),e=Object.assign({},Be,e||{}),i(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n.configure(e)))}return r(n,e),ue(n,[{key:"draw",value:function(){try{var e=this.canvas,i=[-e.drawX,-e.drawY,e.drawWidth,e.drawHeight],r=i[0],o=i[1],a=i[2],l=i[3],s=this.options;if(!e.elementClone.initialized){var d=e.contextClone;d.clearRect(r,o,a,l),d.save(),this.emit("beforePlate"),this.drawBox=F(d,s,r,o,a,l),this.emit("beforeBar"),U.apply(void 0,[d,s].concat(t(this.drawBox))),e.context.barDimensions=d.barDimensions,this.emit("beforeHighlights"),J(d,s),this.emit("beforeMinorTicks"),ee(d,s),this.emit("beforeMajorTicks"),K(d,s),this.emit("beforeNumbers"),te(d,s),this.emit("beforeTitle"),ie(d,s),this.emit("beforeUnits"),re(d,s),e.elementClone.initialized=!0}this.canvas.commit(),e.context.clearRect(r,o,a,l),e.context.save(),e.context.drawImage(e.elementClone,r,o,a,l),e.context.save(),this.emit("beforeProgressBar"),H.apply(void 0,[e.context,s].concat(t(this.drawBox))),this.emit("beforeNeedle"),oe(e.context,s),this.emit("beforeValueBox"),se.apply(void 0,[e.context,s,s.animatedValue?this.options.value:this.value].concat(t(this.drawBox))),ce(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"draw",this).call(this)}catch(e){We.verifyError(e)}return this}}],[{key:"configure",value:function(e){return e.barStrokeWidth>=e.barWidth&&(e.barStrokeWidth=ye(e.barWidth/2)),e.hasLeft=q("right",e),e.hasRight=q("left",e),e.value>e.maxValue&&(e.value=e.maxValue),e.value1&&(d=1),1!==d&&(c=r(d),isFinite(c)&&!isNaN(c)&&(d=c)),t&&t(d),s0){for(a=e.toFixed(i).toString().split("."),n=r-a[0].length;o1?(r=~i.indexOf("."),~i.indexOf("-")?"-"+[t.majorTicksInt+t.majorTicksDec+2+(r?1:0)-i.length].join("0")+i.replace("-",""):[t.majorTicksInt+t.majorTicksDec+1+(r?1:0)-i.length].join("0")+i):i}function m(e){return e*Math.PI/180}function v(e,t){return{x:-e*Math.sin(t),y:e*Math.cos(t)}}function g(e,t,i,r){var o=!(arguments.length>4&&void 0!==arguments[4])||arguments[4],n=arguments.length>5&&void 0!==arguments[5]?arguments[5]:0,a=e.createLinearGradient(o?0:n,o?n:0,o?0:r,o?r:0);return a.addColorStop(0,t),a.addColorStop(1,i),a}function b(e,t){if(arguments.length>2&&void 0!==arguments[2]&&arguments[2])return e.restore(),!0;e.save();var i=t.borderShadowWidth;return i&&(e.shadowBlur=i,e.shadowColor=t.colorBorderShadow),!0}function p(e,t){t.needleShadow&&(e.shadowOffsetX=2,e.shadowOffsetY=2,e.shadowBlur=10,e.shadowColor=t.colorNeedleShadowDown)}function w(e,t,i){return e["font"+t+"Style"]+" "+e["font"+t+"Weight"]+" "+e["font"+t+"Size"]*i+"px "+e["font"+t]}function k(e){e.shadowOffsetX=null,e.shadowOffsetY=null,e.shadowBlur=null,e.shadowColor="",e.strokeStyle=null,e.lineWidth=0,e.save()}function y(e,t,i,r){t.valueTextShadow&&(e.shadowOffsetX=i,e.shadowOffsetY=i,e.shadowBlur=r,e.shadowColor=t.colorValueTextShadow)}function x(e,t,i,r,o,n){if(t.valueBox){k(e);var a=t.valueDec?1+t.valueDec:0,l="9".repeat(Math.max.apply(null,[String(parseInt(i)).length+a].concat(t.majorTicks.map(function(e){return String(parseInt(e,10)).length+a})))),s=t.valueText||u(i,t),d=n/200,c=n/100,f=.4*c,m=1.2*c;e.font=w(t,"Value",d),y(e,t,f,m);var v=e.measureText(t.valueText?s:"-"+u(Number(l),t)).width;k(e);var g=parseFloat(t.fontValueSize)*d+f+m,b=c*parseFloat(t.valueBoxStroke),p=2*n-2*b,x=v+10*c,T=1.1*g+f+m,S=c*t.valueBoxBorderRadius,V=(parseFloat(t.valueBoxWidth)||0)/100*p;V>x&&(x=V),x>p&&(x=p);var W=r-x/2,O=o-T/2,A=o-5.75*c;if(e.beginPath(),S?h(e,W,O,x,T,S):e.rect(W,O,x,T),b){var P=e.createRadialGradient(r,A,10*c,r,A,20*c);P.addColorStop(0,t.colorValueBoxRect),P.addColorStop(1,t.colorValueBoxRectEnd),e.strokeStyle=P,e.lineWidth=b,e.stroke()}t.colorValueBoxShadow&&(e.shadowBlur=1.2*c,e.shadowColor=t.colorValueBoxShadow),t.colorValueBoxBackground&&(e.fillStyle=t.colorValueBoxBackground,e.fill()),e.closePath(),e.restore(),y(e,t,f,m),e.fillStyle=t.colorValueText,e.textAlign="center",e.textBaseline="alphabetic",e.fillText(s,W+x/2,o+T/2-g/3),e.restore()}}function T(e){var t=e.value,i=e.minValue,r=e.maxValue,o=.01*(r-i);return{normal:tr?r:t,indented:tr?r+o:t}}function S(e,t,i,r,o){i.beginPath(),i.arc(0,0,xe(e),0,2*Oe,!0),i.lineWidth=t,i.strokeStyle=o?We.linearGradient(i,r,o,e):r,i.stroke(),i.closePath()}function V(e,t){var i=pe.pixelRatio;return e.maxRadius||(e.maxRadius=e.max-t.borderShadowWidth-t.borderOuterWidth*i-t.borderMiddleWidth*i-t.borderInnerWidth*i+(t.borderOuterWidth?.5:0)+(t.borderMiddleWidth?.5:0)+(t.borderInnerWidth?.5:0)),e.maxRadius}function W(e,t){var i=pe.pixelRatio,r=t.borderShadowWidth*i,o=e.max-r-t.borderOuterWidth*i/2,n=o-t.borderOuterWidth*i/2-t.borderMiddleWidth*i/2+.5,a=n-t.borderMiddleWidth*i/2-t.borderInnerWidth*i/2+.5,l=V(e,t),s=void 0,d=!1;e.save(),t.borderOuterWidth&&(d=We.drawShadow(e,t,d),S(o,t.borderOuterWidth*i,e,t.colorBorderOuter,t.colorBorderOuterEnd)),t.borderMiddleWidth&&(d=We.drawShadow(e,t,d),S(n,t.borderMiddleWidth*i,e,t.colorBorderMiddle,t.colorBorderMiddleEnd)),t.borderInnerWidth&&(d=We.drawShadow(e,t,d),S(a,t.borderInnerWidth*i,e,t.colorBorderInner,t.colorBorderInnerEnd)),We.drawShadow(e,t,d),e.beginPath(),e.arc(0,0,xe(l),0,2*Oe,!0),t.colorPlateEnd?(s=e.createRadialGradient(0,0,l/2,0,0,l),s.addColorStop(0,t.colorPlate),s.addColorStop(1,t.colorPlateEnd)):s=t.colorPlate,e.fillStyle=s,e.fill(),e.closePath(),e.restore()}function O(e,t){var i=e.max*(parseFloat(t.highlightsWidth)||0)/100;if(i){var r=xe(P(e,t)-i/2),o=0,n=t.highlights.length,a=(t.maxValue-t.minValue)/t.ticksAngle;for(e.save();on?o:n,n>o,o>n?i:r):a,t>0?We.roundRect(e,i,r,o,n,t):e.rect(i,r,o,n),e.fill(),e.closePath()}function G(e,t,i,r,o,n,a,l,s){e.beginPath(),e.lineWidth=t,e.strokeStyle=s?We.linearGradient(e,l,s,a,!0,o):l,i>0?We.roundRect(e,r,o,n,a,i):e.rect(r,o,n,a),e.stroke(),e.closePath()}function F(e,t,i,r,o,n){var a=pe.pixelRatio;e.save();var l=t.borderRadius*a,s=o-t.borderShadowWidth-t.borderOuterWidth*a,d=s-t.borderOuterWidth*a-t.borderMiddleWidth*a,c=d-t.borderMiddleWidth*a-t.borderInnerWidth*a,h=c-t.borderInnerWidth*a,u=n-t.borderShadowWidth-t.borderOuterWidth*a,f=u-t.borderOuterWidth*a-t.borderMiddleWidth*a,m=f-t.borderMiddleWidth*a-t.borderInnerWidth*a,v=m-t.borderInnerWidth*a,g=i-(d-s)/2,b=g-(c-d)/2,p=b-(h-c)/2,w=r-(f-u)/2,k=w-(m-f)/2,y=k-(v-m)/2,x=0,T=!1;return t.borderOuterWidth&&(T=We.drawShadow(e,t,T),G(e,t.borderOuterWidth*a,l,i+t.borderOuterWidth*a/2-x,r+t.borderOuterWidth*a/2-x,s,u,t.colorBorderOuter,t.colorBorderOuterEnd),x+=.5*a),t.borderMiddleWidth&&(T=We.drawShadow(e,t,T),G(e,t.borderMiddleWidth*a,l-=1+2*x,g+t.borderMiddleWidth*a/2-x,w+t.borderMiddleWidth*a/2-x,d+2*x,f+2*x,t.colorBorderMiddle,t.colorBorderMiddleEnd),x+=.5*a),t.borderInnerWidth&&(T=We.drawShadow(e,t,T),G(e,t.borderInnerWidth*a,l-=1+2*x,b+t.borderInnerWidth*a/2-x,k+t.borderInnerWidth*a/2-x,c+2*x,m+2*x,t.colorBorderInner,t.colorBorderInnerEnd),x+=.5*a),We.drawShadow(e,t,T),L(e,l,p,y,h+2*x,v+2*x,t.colorPlate,t.colorPlateEnd),e.restore(),[p,y,h,v]}function X(e,t,i,r,o,n){var a=pe.pixelRatio,l=n>=o,s=l?.85*o:n,d=l?n:o;i=l?ye(i+(o-s)/2):i;var c=!!t.title,h=!!t.units,u=!!t.valueBox,f=void 0,m=void 0,v=void 0;l?(m=ye(.05*d),f=ye(.075*d),v=ye(.11*d),c&&(d-=f,r+=f),h&&(d-=m),u&&(d-=v)):(m=f=ye(.15*s),c&&(s-=f,r+=f),h&&(s-=m));var g=2*t.barStrokeWidth,b=t.barBeginCircle?ye(s*t.barBeginCircle/200-g/2):0,p=ye(s*t.barWidth/100-g),w=ye(d*t.barLength/100-g),k=ye((d-w)/2),y=ye(i+(l?s/2:k+b)),x=ye(r+(l?d-k-b+g/2:s/2)),T=!l||t.hasLeft&&t.hasRight?0:(t.hasRight?-1:1)*t.ticksWidth/100*s,S=l||t.hasLeft&&t.hasRight?0:(t.hasRight?-1:1)*t.ticksWidth/100*s;return e.barDimensions={isVertical:l,width:s,length:d,barWidth:p,barLength:w,strokeWidth:g,barMargin:k,radius:b,pixelRatio:a,barOffset:null,titleMargin:c?f:0,unitsMargin:h?m:0,get ticksLength(){return this.barLength-this.barOffset-this.strokeWidth},X:i+T,Y:r+S,x0:y+T,y0:x+S,baseX:i,baseY:r,ticksPadding:t.ticksPadding/100},e.barDimensions}function Y(e,t,i,r,o,n,a){var l=X(e,t,r,o,n,a),s=l.isVertical,d=l.width,c=l.barWidth,h=l.barLength,u=l.strokeWidth,f=l.barMargin,m=l.radius,v=l.x0,g=l.y0,b=l.X,p=l.Y,w=h;if(e.save(),e.beginPath(),t.barBeginCircle){var k=We.radians(s?270:0),y=Math.asin(c/2/m),x=Math.cos(y),T=Math.sin(y),S=v+(s?m*T:m*x-u/2),V=s?g-m*x:g+m*T,W=xe(s?V-g:S-v);e.barDimensions.barOffset=ye(W+m);var O=s?ye(v-m*T):S,A=s?V:ye(g-m*T);"progress"===i&&(h=e.barDimensions.barOffset+(h-e.barDimensions.barOffset)*(We.normalizedValue(t).normal-t.minValue)/(t.maxValue-t.minValue));var P=ye(S+h-e.barDimensions.barOffset+u/2),M=ye(V-h+e.barDimensions.barOffset-u/2);e.arc(v,g,m,k+y,k-y),s?(e.moveTo(S,A),e.lineTo(S,M),e.lineTo(O,M),e.lineTo(O,A)):(e.moveTo(S,A),e.lineTo(P,A),e.lineTo(P,V),e.lineTo(S,V))}else{var B=ye(s?b+(d-c)/2:b+f),C=ye(s?p+h+f:p+(d-c)/2);"progress"===i&&(h*=(t.value-t.minValue)/(t.maxValue-t.minValue)),s?e.rect(B,C,c,-h):e.rect(B,C,h,c)}"progress"!==i&&t.barStrokeWidth&&(e.lineWidth=u,e.strokeStyle=t.colorBarStroke,e.stroke()),"progress"!==i&&t.colorBar?(e.fillStyle=t.colorBarEnd?We.linearGradient(e,t.colorBar,t.colorBarEnd,h,s,s?p:b):t.colorBar,e.fill()):"progress"===i&&t.colorBarProgress&&(e.fillStyle=t.colorBarProgressEnd?We.linearGradient(e,t.colorBarProgress,t.colorBarProgressEnd,w,s,s?p:b):t.colorBarProgress,e.fill()),e.closePath(),e.restore(),t.barBeginCircle&&(e.barDimensions.radius+=u),e.barDimensions.barWidth+=u,e.barDimensions.barLength+=u}function U(e,t,i,r,o,n){Y(e,t,"",i,r,o,n)}function q(e,t){return t.needleSide!==e||t.tickSide!==e||t.numberSide!==e}function H(e,t,i,r,o,n){t.barProgress&&Y(e,t,"progress",i,r,o,n)}function J(e,t){var i=e.barDimensions,r=i.isVertical,o=i.width,n=i.length,a=i.barWidth,l=i.barOffset,s=i.barMargin,d=i.X,c=i.Y,h=i.ticksLength,u=i.ticksPadding,f=o*(parseFloat(t.highlightsWidth)||0)/100;if(t.highlights&&f){var m="right"!==t.tickSide,v="left"!==t.tickSide,g=0,b=t.highlights.length,p=(o-a)/2,w=t.maxValue-t.minValue,k=ye(r?d+p:d+s+l),y=f,x=r?c+n-s-l:c+p,T=ye((t.ticksWidth/100+u)*o)+(f-t.ticksWidth/100*o),S=ye(a+u*o);for(e.save();gn&&(d*=-1),e.moveTo(i-h,r),e.lineTo(i+h,r),e.lineTo(i+h,r+d),e.lineTo(i,n),e.lineTo(i-h,r+d),e.lineTo(i-h,r)):(i>o&&(d*=-1),e.moveTo(i,r-h),e.lineTo(i,r+h),e.lineTo(i+d,r+h),e.lineTo(o,r),e.lineTo(i+d,r-h),e.lineTo(i,r-h)),e.fill(),e.closePath()}function se(e,t,i,r,o,n,a){var l=(parseFloat(t.fontValueSize)||0)*n/200,s=(.11*a-l)/2;e.barDimensions.isVertical&&We.drawValueBox(e,t,i,r+n/2,o+a-l-s,n)}var de=function(){function e(e,t){var i=[],r=!0,o=!1,n=void 0;try{for(var a,l=e[Symbol.iterator]();!(r=(a=l.next()).done)&&(i.push(a.value),!t||i.length!==t);r=!0);}catch(e){o=!0,n=e}finally{try{!r&&l.return&&l.return()}finally{if(o)throw n}}return i}return function(t,i){if(Array.isArray(t))return t;if(Symbol.iterator in Object(t))return e(t,i);throw new TypeError("Invalid attempt to destructure non-iterable instance")}}(),ce=function e(t,i,r){null===t&&(t=Function.prototype);var o=Object.getOwnPropertyDescriptor(t,i);if(void 0===o){var n=Object.getPrototypeOf(t);return null===n?void 0:e(n,i,r)}if("value"in o)return o.value;var a=o.get;if(void 0!==a)return a.call(r)},he=function e(t,i,r,o){var n=Object.getOwnPropertyDescriptor(t,i);if(void 0===n){var a=Object.getPrototypeOf(t);null!==a&&e(a,i,r,o)}else if("value"in n&&n.writable)n.value=r;else{var l=n.set;void 0!==l&&l.call(o,r)}return r},ue=function(){function e(e,t){for(var i=0;i>>0;if(0===o)return-1;var n=+t||0;if(Math.abs(n)===1/0&&(n=0),n>=o)return-1;for(i=Math.max(n>=0?n:o-Math.abs(n),0);i>>0,r=arguments[1],o=r>>0,n=o<0?Math.max(i+o,0):Math.min(o,i),a=arguments[2],l=void 0===a?i:a>>0,s=l<0?Math.max(i+l,0):Math.min(l,i);n1?r-1:0),n=1;n1?t-1:0),r=1;r=(7-4*t)/11)return-Math.pow((11-6*t-11*e)/4,2)+Math.pow(i,2)},elastic:function(e){return 1-ve.delastic(1-e)},delastic:function(e){return Math.pow(2,10*(e-1))*Math.cos(20*Math.PI*1.5/3*e)}},ge=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"linear",i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:250,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){};if(o(this,e),this.duration=i,this.rule=t,this.draw=r,this.end=n,"function"!=typeof this.draw)throw new TypeError("Invalid animation draw callback:",r);if("function"!=typeof this.end)throw new TypeError("Invalid animation end callback:",n)}return ue(e,[{key:"animate",value:function(e,t){var i=this;this.frame&&this.cancel();var r=window.performance&&window.performance.now?window.performance.now():n("animationStartTime")||Date.now();e=e||this.draw,t=t||this.end,this.draw=e,this.end=t,this.frame=me(function(o){return a(o,e,r,ve[i.rule]||i.rule,i.duration,t,i)})}},{key:"cancel",value:function(){if(this.frame){(n("cancelAnimationFrame")||function(e){})(this.frame),this.frame=null}}},{key:"destroy",value:function(){this.cancel(),this.draw=null,this.end=null}}]),e}();ge.rules=ve;var be=function(){function t(i,r,n){o(this,t),this.options=i,this.element=r.toLowerCase(),this.type=t.toDashed(n),this.Type=e[n],this.mutationsObserved=!1,this.isObservable=!!window.MutationObserver,window.GAUGES_NO_AUTO_INIT||t.domReady(this.traverse.bind(this))}return ue(t,[{key:"isValidNode",value:function(e){return!(!e.tagName||e.tagName.toLowerCase()!==this.element||e.getAttribute("data-type")!==this.type)}},{key:"traverse",value:function(){for(var e=document.getElementsByTagName(this.element),t=0,i=e.length;t1&&void 0!==arguments[1])||arguments[1],i=e.split(/-/),r=0,o=i.length,n="";r1&&void 0!==arguments[1]?arguments[1]:0;return e=parseFloat(e),!isNaN(e)&&isFinite(e)||(e=parseFloat(t)||0),e}},{key:"mod",value:function(e,t){return(e%t+t)%t}},{key:"version",get:function(){return ke}}]),n}(fe);void 0!==e&&(e.BaseGauge=Se,e.gauges=(window.document||{}).gauges=Te);var Ve=/{([_a-zA-Z]+[_a-zA-Z0-9]*)}/g,We={roundRect:h,padValue:u,formatMajorTickNumber:f,radians:m,radialPoint:v,linearGradient:g,drawNeedleShadow:p,drawValueBox:x,verifyError:s,prepareTicks:c,drawShadow:b,font:w,normalizedValue:T,formatContext:d},Oe=Math.PI,Ae=Oe/2,Pe=Object.assign({},we,{ticksAngle:270,startAngle:45,colorNeedleCircleOuter:"#f0f0f0",colorNeedleCircleOuterEnd:"#ccc",colorNeedleCircleInner:"#e8e8e8",colorNeedleCircleInnerEnd:"#f5f5f5",needleCircleSize:10,needleCircleInner:!0,needleCircleOuter:!0,needleStart:20,animationTarget:"needle",useMinPath:!1,barWidth:0,barStartPosition:"left"}),Me=function(e){function t(e){return o(this,t),e=Object.assign({},Pe,e||{}),i(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,t.configure(e)))}return r(t,e),ue(t,[{key:"draw",value:function(){try{var e=this.canvas,i=[-e.drawX,-e.drawY,e.drawWidth,e.drawHeight],r=i[0],o=i[1],n=i[2],a=i[3],l=this.options;if("needle"===l.animationTarget){if(!e.elementClone.initialized){var s=e.contextClone;s.clearRect(r,o,n,a),s.save(),this.emit("beforePlate"),W(s,l),this.emit("beforeHighlights"),O(s,l),this.emit("beforeMinorTicks"),A(s,l),this.emit("beforeMajorTicks"),M(s,l),this.emit("beforeNumbers"),j(s,l),this.emit("beforeTitle"),N(s,l),this.emit("beforeUnits"),E(s,l),e.elementClone.initialized=!0}this.canvas.commit(),e.context.clearRect(r,o,n,a),e.context.drawImage(e.elementClone,r,o,n,a),e.context.save(),this.emit("beforeProgressBar"),D(e.context,l),this.emit("beforeValueBox"),R(e.context,l,z(this)),this.emit("beforeNeedle"),_(e.context,l)}else{var d=-We.radians((l.value-l.minValue)/(l.maxValue-l.minValue)*l.ticksAngle);if(e.context.clearRect(r,o,n,a),e.context.save(),this.emit("beforePlate"),W(e.context,l),e.context.rotate(d),this.emit("beforeHighlights"),O(e.context,l),this.emit("beforeMinorTicks"),A(e.context,l),this.emit("beforeMajorTicks"),M(e.context,l),this.emit("beforeNumbers"),j(e.context,l),this.emit("beforeProgressBar"),D(e.context,l),e.context.rotate(-d),e.context.save(),!e.elementClone.initialized){var c=e.contextClone;c.clearRect(r,o,n,a),c.save(),this.emit("beforeTitle"),N(c,l),this.emit("beforeUnits"),E(c,l),this.emit("beforeNeedle"),_(c,l),e.elementClone.initialized=!0}e.context.drawImage(e.elementClone,r,o,n,a)}this.emit("beforeValueBox"),R(e.context,l,z(this)),ce(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"draw",this).call(this)}catch(e){We.verifyError(e)}return this}},{key:"value",set:function(e){e=Se.ensureValue(e,this.options.minValue),this.options.animation&&360===this.options.ticksAngle&&this.options.useMinPath&&(this._value=e,e=this.options.value+((e-this.options.value)%360+540)%360-180),he(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"value",e,this)},get:function(){return ce(t.prototype.__proto__||Object.getPrototypeOf(t.prototype),"value",this)}}],[{key:"configure",value:function(e){return e.barWidth>50&&(e.barWidth=50),isNaN(e.startAngle)&&(e.startAngle=45),isNaN(e.ticksAngle)&&(e.ticksAngle=270),e.ticksAngle>360&&(e.ticksAngle=360),e.ticksAngle<0&&(e.ticksAngle=0),e.startAngle<0&&(e.startAngle=0),e.startAngle>360&&(e.startAngle=360),e}}]),t}(Se);void 0!==e&&(e.RadialGauge=Me),Se.initialize("RadialGauge",Pe);var Be=Object.assign({},we,{borderRadius:0,barBeginCircle:30,colorBarEnd:"",colorBarProgressEnd:"",needleWidth:6,tickSide:"both",needleSide:"both",numberSide:"both",ticksWidth:10,ticksWidthMinor:5,ticksPadding:5,barLength:85,fontTitleSize:26,highlightsWidth:10}),Ce=function(e){function n(e){return o(this,n),e=Object.assign({},Be,e||{}),i(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,n.configure(e)))}return r(n,e),ue(n,[{key:"draw",value:function(){try{var e=this.canvas,i=[-e.drawX,-e.drawY,e.drawWidth,e.drawHeight],r=i[0],o=i[1],a=i[2],l=i[3],s=this.options;if(!e.elementClone.initialized){var d=e.contextClone;d.clearRect(r,o,a,l),d.save(),this.emit("beforePlate"),this.drawBox=F(d,s,r,o,a,l),this.emit("beforeBar"),U.apply(void 0,[d,s].concat(t(this.drawBox))),e.context.barDimensions=d.barDimensions,this.emit("beforeHighlights"),J(d,s),this.emit("beforeMinorTicks"),ee(d,s),this.emit("beforeMajorTicks"),K(d,s),this.emit("beforeNumbers"),te(d,s),this.emit("beforeTitle"),ie(d,s),this.emit("beforeUnits"),re(d,s),e.elementClone.initialized=!0}this.canvas.commit(),e.context.clearRect(r,o,a,l),e.context.drawImage(e.elementClone,r,o,a,l),this.emit("beforeProgressBar"),H.apply(void 0,[e.context,s].concat(t(this.drawBox))),this.emit("beforeNeedle"),oe(e.context,s),this.emit("beforeValueBox"),se.apply(void 0,[e.context,s,s.animatedValue?this.options.value:this.value].concat(t(this.drawBox))),ce(n.prototype.__proto__||Object.getPrototypeOf(n.prototype),"draw",this).call(this)}catch(e){We.verifyError(e)}return this}}],[{key:"configure",value:function(e){return e.barStrokeWidth>=e.barWidth&&(e.barStrokeWidth=ye(e.barWidth/2)),e.hasLeft=q("right",e),e.hasRight=q("left",e),e.value>e.maxValue&&(e.value=e.maxValue),e.value4.0.0 org.thingsboard - 3.3.2-SNAPSHOT + 3.3.3-SNAPSHOT thingsboard org.thingsboard @@ -56,8 +56,8 @@ install-node-and-yarn - v12.16.1 - v1.22.4 + v16.13.0 + v1.22.17 diff --git a/ui-ngx/proxy.conf.js b/ui-ngx/proxy.conf.js index 1211280966..2e3817445b 100644 --- a/ui-ngx/proxy.conf.js +++ b/ui-ngx/proxy.conf.js @@ -26,10 +26,6 @@ const PROXY_CONFIG = { "target": ruleNodeUiforwardUrl, "secure": false, }, - "/assets/help/*/rulenode/**": { - "target": ruleNodeUiforwardUrl, - "secure": false, - }, "/static/widgets": { "target": forwardUrl, "secure": false, diff --git a/ui-ngx/src/.browserslistrc b/ui-ngx/src/.browserslistrc deleted file mode 100644 index 0209813ebf..0000000000 --- a/ui-ngx/src/.browserslistrc +++ /dev/null @@ -1,11 +0,0 @@ -# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries -# -# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed - -> 0.5% -last 2 versions -Firefox ESR -not dead -IE 11 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 bb2fd9e82a..d053efb601 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -235,6 +235,7 @@ export interface WidgetSubscriptionOptions { stateData?: boolean; alarmSource?: Datasource; datasources?: Array; + datasourcesOptional?: boolean; hasDataPageLink?: boolean; singleEntity?: boolean; warnOnPageDataOverflow?: boolean; @@ -282,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 3346d5ce83..2b71df5a0e 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -906,28 +906,12 @@ export class EntityService { result.entityFilter = deepClone(filter); return of(result); case AliasFilterType.relationsQuery: - result.stateEntity = filter.rootStateEntity; - let rootEntityType; - let rootEntityId; - if (result.stateEntity && stateEntityId) { - rootEntityType = stateEntityId.entityType; - rootEntityId = stateEntityId.id; - } else if (!result.stateEntity) { - rootEntityType = filter.rootEntity.entityType; - rootEntityId = filter.rootEntity.id; - } - if (rootEntityType && rootEntityId) { - const relationQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); - result.entityFilter = deepClone(filter); - result.entityFilter.rootEntity = relationQueryRootEntityId; - return of(result); - } else { - return of(result); - } case AliasFilterType.assetSearchQuery: case AliasFilterType.deviceSearchQuery: case AliasFilterType.edgeSearchQuery: case AliasFilterType.entityViewSearchQuery: + let rootEntityType; + let rootEntityId; result.stateEntity = filter.rootStateEntity; if (result.stateEntity && stateEntityId) { rootEntityType = stateEntityId.entityType; @@ -937,9 +921,9 @@ export class EntityService { rootEntityId = filter.rootEntity.id; } if (rootEntityType && rootEntityId) { - const searchQueryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); + const queryRootEntityId = this.resolveAliasEntityId(rootEntityType, rootEntityId); result.entityFilter = deepClone(filter); - result.entityFilter.rootEntity = searchQueryRootEntityId; + result.entityFilter.rootEntity = queryRootEntityId; return of(result); } else { return of(result); diff --git a/ui-ngx/src/app/core/http/rule-chain.service.ts b/ui-ngx/src/app/core/http/rule-chain.service.ts index e65e7017e0..3fa9168d5f 100644 --- a/ui-ngx/src/app/core/http/rule-chain.service.ts +++ b/ui-ngx/src/app/core/http/rule-chain.service.ts @@ -21,11 +21,8 @@ import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { - ResolvedRuleChainMetaData, RuleChain, - RuleChainConnectionInfo, RuleChainMetaData, - ruleChainNodeComponent, RuleChainType, ruleNodeTypeComponentTypes, unknownNodeComponent @@ -34,17 +31,17 @@ import { ComponentDescriptorService } from './component-descriptor.service'; import { IRuleNodeConfigurationComponent, LinkLabel, - RuleNodeComponentDescriptor, + RuleNodeComponentDescriptor, RuleNodeConfiguration, TestScriptInputParams, TestScriptResult } from '@app/shared/models/rule-node.models'; import { ResourcesService } from '../services/resources.service'; import { catchError, map, mergeMap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; -import { EntityType } from '@shared/models/entity-type.models'; import { deepClone, snakeCase } from '@core/utils'; import { DebugRuleNodeEventBody } from '@app/shared/models/event.models'; import { Edge } from '@shared/models/edge.models'; +import { IModulesMap } from '@modules/common/modules-map.models'; @Injectable({ providedIn: 'root' @@ -62,7 +59,8 @@ export class RuleChainService { private translate: TranslateService ) { } - public getRuleChains(pageLink: PageLink, type: RuleChainType = RuleChainType.CORE, config?: RequestConfig): Observable> { + public getRuleChains(pageLink: PageLink, type: RuleChainType = RuleChainType.CORE, + config?: RequestConfig): Observable> { return this.http.get>(`/api/ruleChains${pageLink.toQuery()}&type=${type}`, defaultHttpOptionsFromConfig(config)); } @@ -71,6 +69,10 @@ export class RuleChainService { return this.http.get(`/api/ruleChain/${ruleChainId}`, defaultHttpOptionsFromConfig(config)); } + public getRuleChainOutputLabels(ruleChainId: string, config?: RequestConfig): Observable> { + return this.http.get>(`/api/ruleChain/${ruleChainId}/output/labels`, defaultHttpOptionsFromConfig(config)); + } + public createDefaultRuleChain(ruleChainName: string, config?: RequestConfig): Observable { return this.http.post('/api/ruleChain/device/default', { name: ruleChainName @@ -93,43 +95,20 @@ export class RuleChainService { return this.http.get(`/api/ruleChain/${ruleChainId}/metadata`, defaultHttpOptionsFromConfig(config)); } - public getResolvedRuleChainMetadata(ruleChainId: string, config?: RequestConfig): Observable { - return this.getRuleChainMetadata(ruleChainId, config).pipe( - mergeMap((ruleChainMetaData) => this.resolveRuleChainMetadata(ruleChainMetaData)) - ); - } - public saveRuleChainMetadata(ruleChainMetaData: RuleChainMetaData, config?: RequestConfig): Observable { return this.http.post('/api/ruleChain/metadata', ruleChainMetaData, defaultHttpOptionsFromConfig(config)); } - public saveAndGetResolvedRuleChainMetadata(ruleChainMetaData: RuleChainMetaData, - config?: RequestConfig): Observable { - return this.saveRuleChainMetadata(ruleChainMetaData, config).pipe( - mergeMap((savedRuleChainMetaData) => this.resolveRuleChainMetadata(savedRuleChainMetaData)) - ); - } - - public resolveRuleChainMetadata(ruleChainMetaData: RuleChainMetaData): Observable { - return this.resolveTargetRuleChains(ruleChainMetaData.ruleChainConnections).pipe( - map((targetRuleChainsMap) => { - const resolvedRuleChainMetadata: ResolvedRuleChainMetaData = {...ruleChainMetaData, targetRuleChainsMap}; - return resolvedRuleChainMetadata; - }) - ); - } - - public getRuleNodeComponents(ruleNodeConfigResourcesModulesMap: {[key: string]: any}, ruleChainType: RuleChainType, config?: RequestConfig): + public getRuleNodeComponents(modulesMap: IModulesMap, ruleChainType: RuleChainType, config?: RequestConfig): Observable> { if (this.ruleNodeComponentsMap.get(ruleChainType)) { return of(this.ruleNodeComponentsMap.get(ruleChainType)); } else { return this.loadRuleNodeComponents(ruleChainType, config).pipe( mergeMap((components) => { - return this.resolveRuleNodeComponentsUiResources(components, ruleNodeConfigResourcesModulesMap).pipe( + return this.resolveRuleNodeComponentsUiResources(components, modulesMap).pipe( map((ruleNodeComponents) => { this.ruleNodeComponentsMap.set(ruleChainType, ruleNodeComponents); - this.ruleNodeComponentsMap.get(ruleChainType).push(ruleChainNodeComponent); this.ruleNodeComponentsMap.get(ruleChainType).sort( (comp1, comp2) => { let result = comp1.type.toString().localeCompare(comp2.type.toString()); @@ -179,6 +158,14 @@ export class RuleChainService { return component.configurationDescriptor.nodeDefinition.customRelations; } + public ruleNodeSourceRuleChainId(component: RuleNodeComponentDescriptor, config: RuleNodeConfiguration): string { + if (component.configurationDescriptor.nodeDefinition.ruleChainNode) { + return config?.ruleChainId; + } else { + return null; + } + } + public getLatestRuleNodeDebugInput(ruleNodeId: string, config?: RequestConfig): Observable { return this.http.get(`/api/ruleNode/${ruleNodeId}/debugIn`, defaultHttpOptionsFromConfig(config)); } @@ -187,26 +174,6 @@ export class RuleChainService { return this.http.post('/api/ruleChain/testScript', inputParams, defaultHttpOptionsFromConfig(config)); } - private resolveTargetRuleChains(ruleChainConnections: Array): Observable<{[ruleChainId: string]: RuleChain}> { - if (ruleChainConnections && ruleChainConnections.length) { - const tasks: Observable[] = []; - ruleChainConnections.forEach((connection) => { - tasks.push(this.resolveRuleChain(connection.targetRuleChainId.id)); - }); - return forkJoin(tasks).pipe( - map((ruleChains) => { - const ruleChainsMap: {[ruleChainId: string]: RuleChain} = {}; - ruleChains.forEach((ruleChain) => { - ruleChainsMap[ruleChain.id.id] = ruleChain; - }); - return ruleChainsMap; - }) - ); - } else { - return of({} as {[ruleChainId: string]: RuleChain}); - } - } - private loadRuleNodeComponents(ruleChainType: RuleChainType, config?: RequestConfig): Observable> { return this.componentDescriptorService.getComponentDescriptorsByTypes(ruleNodeTypeComponentTypes, ruleChainType, config).pipe( map((components) => { @@ -220,21 +187,21 @@ export class RuleChainService { } private resolveRuleNodeComponentsUiResources(components: Array, - ruleNodeConfigResourcesModulesMap: {[key: string]: any}): + modulesMap: IModulesMap): Observable> { const tasks: Observable[] = []; components.forEach((component) => { - tasks.push(this.resolveRuleNodeComponentUiResources(component, ruleNodeConfigResourcesModulesMap)); + tasks.push(this.resolveRuleNodeComponentUiResources(component, modulesMap)); }); return forkJoin(tasks).pipe( - catchError((err) => { + catchError(() => { return of(components); }) ); } private resolveRuleNodeComponentUiResources(component: RuleNodeComponentDescriptor, - ruleNodeConfigResourcesModulesMap: {[key: string]: any}): + modulesMap: IModulesMap): Observable { const nodeDefinition = component.configurationDescriptor.nodeDefinition; const uiResources = nodeDefinition.uiResources; @@ -248,7 +215,7 @@ export class RuleChainService { }); } if (moduleResource) { - tasks.push(this.resourcesService.loadFactories(moduleResource, ruleNodeConfigResourcesModulesMap).pipe( + tasks.push(this.resourcesService.loadFactories(moduleResource, modulesMap).pipe( map((res) => { if (nodeDefinition.configDirective && nodeDefinition.configDirective.length) { const selector = snakeCase(nodeDefinition.configDirective, '-'); @@ -267,7 +234,7 @@ export class RuleChainService { )); } return forkJoin(tasks).pipe( - map((res) => { + map(() => { return component; }), catchError(() => { @@ -280,21 +247,6 @@ export class RuleChainService { } } - private resolveRuleChain(ruleChainId: string): Observable { - return this.getRuleChain(ruleChainId, {ignoreErrors: true}).pipe( - map(ruleChain => ruleChain), - catchError((err) => { - const ruleChain = { - id: { - entityType: EntityType.RULE_CHAIN, - id: ruleChainId - } - } as RuleChain; - return of(ruleChain); - }) - ); - } - public getEdgeRuleChains(edgeId: string, pageLink: PageLink, config?: RequestConfig): Observable> { return this.http.get>(`/api/edge/${edgeId}/ruleChains${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); 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/services/help.service.ts b/ui-ngx/src/app/core/services/help.service.ts index 2ed8863b4d..df8b14d71c 100644 --- a/ui-ngx/src/app/core/services/help.service.ts +++ b/ui-ngx/src/app/core/services/help.service.ts @@ -18,23 +18,29 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { Observable, of } from 'rxjs'; -import { catchError, mergeMap, tap } from 'rxjs/operators'; -import { helpBaseUrl } from '@shared/models/constants'; +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 NOT_FOUND_CONTENT = '## Not found'; +const localHelpBaseUrl = '/assets'; + +const NOT_FOUND_CONTENT: HelpData = { + content: '## Not found', + helpBaseUrl: localHelpBaseUrl +}; @Injectable({ providedIn: 'root' }) export class HelpService { - private helpBaseUrl = helpBaseUrl; - + private siteBaseUrl = siteBaseUrl; private helpCache: {[lang: string]: {[key: string]: string}} = {}; constructor( private translate: TranslateService, - private http: HttpClient + private http: HttpClient, + private uiSettingsService: UiSettingsService ) {} getHelpContent(key: string): Observable { @@ -70,13 +76,38 @@ export class HelpService { } } - private loadHelpContent(lang: string, key: string): Observable { - return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} ); + 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(content: string): string { - const baseUrlReg = /\${baseUrl}/g; - return content.replace(baseUrlReg, this.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 { @@ -96,3 +127,8 @@ export class HelpService { } } + +interface HelpData { + content: string; + helpBaseUrl: string; +} diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index e87f16d885..f6cd757bbb 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -26,8 +26,9 @@ import { import { DOCUMENT } from '@angular/common'; import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs'; import { HttpClient } from '@angular/common/http'; +import { IModulesMap } from '@modules/common/modules-map.models'; -declare const SystemJS; +declare const System; @Injectable({ providedIn: 'root' @@ -63,23 +64,19 @@ export class ResourcesService { return this.loadResourceByType(fileType, url); } - public loadFactories(url: string, modulesMap: {[key: string]: any}): Observable[]> { + public loadFactories(url: string, modulesMap: IModulesMap): Observable[]> { if (this.loadedFactories[url]) { return this.loadedFactories[url].asObservable(); } + modulesMap.init(); const subject = new ReplaySubject[]>(); this.loadedFactories[url] = subject; - if (modulesMap) { - for (const moduleId of Object.keys(modulesMap)) { - SystemJS.set(moduleId, modulesMap[moduleId]); - } - } - SystemJS.import(url).then( - (module) => { - const modules = this.extractNgModules(module); - if (modules.length) { - import('@angular/compiler').then( - () => { + import('@angular/compiler').then( + () => { + System.import(url).then( + (module) => { + const modules = this.extractNgModules(module); + if (modules.length) { const tasks: Promise>[] = []; for (const m of modules) { tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m)); @@ -101,44 +98,41 @@ export class ResourcesService { (e) => { this.loadedFactories[url].error(new Error(`Unable to compile module from url: ${url}`)); delete this.loadedFactories[url]; - }); } - ); - } else { - this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); - delete this.loadedFactories[url]; - } - }, - (e) => { - this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedFactories[url]; + }); + } else { + this.loadedFactories[url].error(new Error(`Module '${url}' doesn't have default export!`)); + delete this.loadedFactories[url]; + } + }, + (e) => { + this.loadedFactories[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedFactories[url]; + } + ); } ); return subject.asObservable(); } - public loadModules(url: string, modulesMap: {[key: string]: any}): Observable[]> { + public loadModules(url: string, modulesMap: IModulesMap): Observable[]> { if (this.loadedModules[url]) { return this.loadedModules[url].asObservable(); } + modulesMap.init(); const subject = new ReplaySubject[]>(); this.loadedModules[url] = subject; - if (modulesMap) { - for (const moduleId of Object.keys(modulesMap)) { - SystemJS.set(moduleId, modulesMap[moduleId]); - } - } - SystemJS.import(url).then( - (module) => { - try { - let modules; - try { - modules = this.extractNgModules(module); - } catch (e) { - console.error(e); - } - if (modules && modules.length) { - import('@angular/compiler').then( - () => { + import('@angular/compiler').then( + () => { + System.import(url).then( + (module) => { + try { + let modules; + try { + modules = this.extractNgModules(module); + } catch (e) { + console.error(e); + } + if (modules && modules.length) { const tasks: Promise>[] = []; for (const m of modules) { tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m)); @@ -159,21 +153,21 @@ export class ResourcesService { this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`)); delete this.loadedModules[url]; }); + } else { + this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`)); + delete this.loadedModules[url]; } - ); - } else { - this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`)); + } catch (e) { + this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); + delete this.loadedModules[url]; + } + }, + (e) => { + this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); delete this.loadedModules[url]; + console.error(`Unable to load module from url: ${url}`, e); } - } catch (e) { - this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedModules[url]; - } - }, - (e) => { - this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`)); - delete this.loadedModules[url]; - console.error(`Unable to load module from url: ${url}`, e); + ); } ); return subject.asObservable(); @@ -184,24 +178,23 @@ export class ResourcesService { let potentialModules = [module]; let currentScanDepth = 0; - while(potentialModules.length && currentScanDepth < 10) { - let newPotentialModules = []; - for (const module of potentialModules) { - if (module && 'ɵmod' in module) { - modules.push(module); + while (potentialModules.length && currentScanDepth < 10) { + const newPotentialModules = []; + for (const potentialModule of potentialModules) { + if (potentialModule && ('ɵmod' in potentialModule)) { + modules.push(potentialModule); } else { - for (const k of Object.keys(module)) { - if(!this.isPrimitive(module[k])) { - newPotentialModules.push(module[k]); + for (const k of Object.keys(potentialModule)) { + if (!this.isPrimitive(potentialModule[k])) { + newPotentialModules.push(potentialModule[k]); } } } } - potentialModules = newPotentialModules; currentScanDepth++; } - } catch(e) { + } catch (e) { console.log('Could not load NgModule', e); } return modules; diff --git a/ui-ngx/src/typings/add-marker.d.ts b/ui-ngx/src/app/modules/common/modules-map.models.ts similarity index 65% rename from ui-ngx/src/typings/add-marker.d.ts rename to ui-ngx/src/app/modules/common/modules-map.models.ts index daef6bada9..436890554e 100644 --- a/ui-ngx/src/typings/add-marker.d.ts +++ b/ui-ngx/src/app/modules/common/modules-map.models.ts @@ -14,18 +14,6 @@ /// limitations under the License. /// -import * as L from 'leaflet'; - -declare module 'leaflet' { - - namespace Control { - class AddMarker extends L.Control { } - class AddPolygon extends L.Control { } - } - - namespace control { - function addMarker(options): Control.AddMarker; - function addPolygon(options): Control.AddPolygon; - } - +export interface IModulesMap { + init(): void; } diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index dcb4289934..8404a03aa0 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -19,6 +19,9 @@ import * as AngularCore from '@angular/core'; import * as AngularCommon from '@angular/common'; import * as AngularForms from '@angular/forms'; import * as AngularFlexLayout from '@angular/flex-layout'; +import * as AngularFlexLayoutFlex from '@angular/flex-layout/flex'; +import * as AngularFlexLayoutGrid from '@angular/flex-layout/grid'; +import * as AngularFlexLayoutExtended from '@angular/flex-layout/extended'; import * as AngularPlatformBrowser from '@angular/platform-browser'; import * as AngularRouter from '@angular/router'; import * as AngularCdkCoercion from '@angular/cdk/coercion'; @@ -62,76 +65,520 @@ import * as AngularMaterialTabs from '@angular/material/tabs'; import * as AngularMaterialToolbar from '@angular/material/toolbar'; import * as AngularMaterialTooltip from '@angular/material/tooltip'; import * as AngularMaterialTree from '@angular/material/tree'; +import * as DragDropModule from '@angular/cdk/drag-drop'; +import * as HttpClientModule from '@angular/common/http'; + import * as NgrxStore from '@ngrx/store'; import * as RxJs from 'rxjs'; import * as RxJsOperators from 'rxjs/operators'; import * as TranslateCore from '@ngx-translate/core'; +import * as _moment from 'moment'; + import * as TbCore from '@core/public-api'; import * as TbShared from '@shared/public-api'; import * as TbHomeComponents from '@home/components/public-api'; -import * as _moment from 'moment'; -import * as DragDropModule from "@angular/cdk/drag-drop"; -import * as HttpClientModule from "@angular/common/http"; - -declare const SystemJS; - -export const modulesMap: {[key: string]: any} = { - '@angular/animations': SystemJS.newModule(AngularAnimations), - '@angular/core': SystemJS.newModule(AngularCore), - '@angular/common': SystemJS.newModule(AngularCommon), - '@angular/common/http': SystemJS.newModule(HttpClientModule), - '@angular/forms': SystemJS.newModule(AngularForms), - '@angular/flex-layout': SystemJS.newModule(AngularFlexLayout), - '@angular/platform-browser': SystemJS.newModule(AngularPlatformBrowser), - '@angular/router': SystemJS.newModule(AngularRouter), - '@angular/cdk/coercion': SystemJS.newModule(AngularCdkCoercion), - '@angular/cdk/collections': SystemJS.newModule(AngularCdkCollections), - '@angular/cdk/keycodes': SystemJS.newModule(AngularCdkKeycodes), - '@angular/cdk/layout': SystemJS.newModule(AngularCdkLayout), - '@angular/cdk/overlay': SystemJS.newModule(AngularCdkOverlay), - '@angular/cdk/portal': SystemJS.newModule(AngularCdkPortal), - '@angular/cdk/drag-drop': SystemJS.newModule(DragDropModule), - '@angular/material/autocomplete': SystemJS.newModule(AngularMaterialAutocomplete), - '@angular/material/badge': SystemJS.newModule(AngularMaterialBadge), - '@angular/material/bottom-sheet': SystemJS.newModule(AngularMaterialBottomSheet), - '@angular/material/button': SystemJS.newModule(AngularMaterialButton), - '@angular/material/button-toggle': SystemJS.newModule(AngularMaterialButtonToggle), - '@angular/material/card': SystemJS.newModule(AngularMaterialCard), - '@angular/material/checkbox': SystemJS.newModule(AngularMaterialCheckbox), - '@angular/material/chips': SystemJS.newModule(AngularMaterialChips), - '@angular/material/core': SystemJS.newModule(AngularMaterialCore), - '@angular/material/datepicker': SystemJS.newModule(AngularMaterialDatepicker), - '@angular/material/dialog': SystemJS.newModule(AngularMaterialDialog), - '@angular/material/divider': SystemJS.newModule(AngularMaterialDivider), - '@angular/material/expansion': SystemJS.newModule(AngularMaterialExpansion), - '@angular/material/form-field': SystemJS.newModule(AngularMaterialFormField), - '@angular/material/grid-list': SystemJS.newModule(AngularMaterialGridList), - '@angular/material/icon': SystemJS.newModule(AngularMaterialIcon), - '@angular/material/input': SystemJS.newModule(AngularMaterialInput), - '@angular/material/list': SystemJS.newModule(AngularMaterialList), - '@angular/material/menu': SystemJS.newModule(AngularMaterialMenu), - '@angular/material/paginator': SystemJS.newModule(AngularMaterialPaginator), - '@angular/material/progress-bar': SystemJS.newModule(AngularMaterialProgressBar), - '@angular/material/progress-spinner': SystemJS.newModule(AngularMaterialProgressSpinner), - '@angular/material/radio': SystemJS.newModule(AngularMaterialRadio), - '@angular/material/select': SystemJS.newModule(AngularMaterialSelect), - '@angular/material/sidenav': SystemJS.newModule(AngularMaterialSidenav), - '@angular/material/slide-toggle': SystemJS.newModule(AngularMaterialSlideToggle), - '@angular/material/slider': SystemJS.newModule(AngularMaterialSlider), - '@angular/material/snack-bar': SystemJS.newModule(AngularMaterialSnackBar), - '@angular/material/sort': SystemJS.newModule(AngularMaterialSort), - '@angular/material/stepper': SystemJS.newModule(AngularMaterialStepper), - '@angular/material/table': SystemJS.newModule(AngularMaterialTable), - '@angular/material/tabs': SystemJS.newModule(AngularMaterialTabs), - '@angular/material/toolbar': SystemJS.newModule(AngularMaterialToolbar), - '@angular/material/tooltip': SystemJS.newModule(AngularMaterialTooltip), - '@angular/material/tree': SystemJS.newModule(AngularMaterialTree), - '@ngrx/store': SystemJS.newModule(NgrxStore), - rxjs: SystemJS.newModule(RxJs), - 'rxjs/operators': SystemJS.newModule(RxJsOperators), - '@ngx-translate/core': SystemJS.newModule(TranslateCore), - '@core/public-api': SystemJS.newModule(TbCore), - '@shared/public-api': SystemJS.newModule(TbShared), - '@home/components/public-api': SystemJS.newModule(TbHomeComponents), - moment: SystemJS.newModule(_moment) -}; + +import * as MillisecondsToTimeStringPipe from '@shared/pipe/milliseconds-to-time-string.pipe'; +import * as EnumToArrayPipe from '@shared/pipe/enum-to-array.pipe'; +import * as HighlightPipe from '@shared/pipe/highlight.pipe'; +import * as TruncatePipe from '@shared/pipe/truncate.pipe'; +import * as TbJsonPipe from '@shared/pipe/tbJson.pipe'; +import * as FileSizePipe from '@shared/pipe/file-size.pipe'; +import * as NospacePipe from '@shared/pipe/nospace.pipe'; +import * as SelectableColumnsPipe from '@shared/pipe/selectable-columns.pipe'; +import * as KeyboardShortcutPipe from '@shared/pipe/keyboard-shortcut.pipe'; + +import * as FooterComponent from '@shared/components/footer.component'; +import * as LogoComponent from '@shared/components/logo.component'; +import * as FooterFabButtonsComponent from '@shared/components/footer-fab-buttons.component'; +import * as FullscreenDirective from '@shared/components/fullscreen.directive'; +import * as CircularProgressDirective from '@shared/components/circular-progress.directive'; +import * as MatChipDraggableDirective from '@shared/components/mat-chip-draggable.directive'; +import * as TbHotkeysDirective from '@shared/components/hotkeys.directive'; +import * as TbAnchorComponent from '@shared/components/tb-anchor.component'; +import * as TbPopoverComponent from '@shared/components/popover.component'; +import * as TbStringTemplateOutletDirective from '@shared/components/directives/sring-template-outlet.directive'; +import * as TbComponentOutletDirective from '@shared/components/directives/component-outlet.directive'; +import * as TbMarkdownComponent from '@shared/components/markdown.component'; +import * as HelpComponent from '@shared/components/help.component'; +import * as HelpMarkdownComponent from '@shared/components/help-markdown.component'; +import * as HelpPopupComponent from '@shared/components/help-popup.component'; +import * as TbCheckboxComponent from '@shared/components/tb-checkbox.component'; +import * as TbToast from '@shared/components/toast.directive'; +import * as TbErrorComponent from '@shared/components/tb-error.component'; +import * as TbCheatSheetComponent from '@shared/components/cheatsheet.component'; +import * as BreadcrumbComponent from '@shared/components/breadcrumb.component'; +import * as UserMenuComponent from '@shared/components/user-menu.component'; +import * as TimewindowComponent from '@shared/components/time/timewindow.component'; +import * as TimewindowPanelComponent from '@shared/components/time/timewindow-panel.component'; +import * as TimeintervalComponent from '@shared/components/time/timeinterval.component'; +import * as QuickTimeIntervalComponent from '@shared/components/time/quick-time-interval.component'; +import * as DashboardSelectComponent from '@shared/components/dashboard-select.component'; +import * as DashboardSelectPanelComponent from '@shared/components/dashboard-select-panel.component'; +import * as DatetimePeriodComponent from '@shared/components/time/datetime-period.component'; +import * as DatetimeComponent from '@shared/components/time/datetime.component'; +import * as TimezoneSelectComponent from '@shared/components/time/timezone-select.component'; +import * as ValueInputComponent from '@shared/components/value-input.component'; +import * as DashboardAutocompleteComponent from '@shared/components/dashboard-autocomplete.component'; +import * as EntitySubTypeAutocompleteComponent from '@shared/components/entity/entity-subtype-autocomplete.component'; +import * as EntitySubTypeSelectComponent from '@shared/components/entity/entity-subtype-select.component'; +import * as EntitySubTypeListComponent from '@shared/components/entity/entity-subtype-list.component'; +import * as EntityAutocompleteComponent from '@shared/components/entity/entity-autocomplete.component'; +import * as EntityListComponent from '@shared/components/entity/entity-list.component'; +import * as EntityTypeSelectComponent from '@shared/components/entity/entity-type-select.component'; +import * as EntitySelectComponent from '@shared/components/entity/entity-select.component'; +import * as EntityKeysListComponent from '@shared/components/entity/entity-keys-list.component'; +import * as EntityListSelectComponent from '@shared/components/entity/entity-list-select.component'; +import * as EntityTypeListComponent from '@shared/components/entity/entity-type-list.component'; +import * as QueueTypeListComponent from '@shared/components/queue/queue-type-list.component'; +import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component'; +import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component'; +import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component'; +import * as JsonContentComponent from '@shared/components/json-content.component'; +import * as JsFuncComponent from '@shared/components/js-func.component'; +import * as FabToolbarComponent from '@shared/components/fab-toolbar.component'; +import * as WidgetsBundleSelectComponent from '@shared/components/widgets-bundle-select.component'; +import * as ConfirmDialogComponent from '@shared/components/dialog/confirm-dialog.component'; +import * as AlertDialogComponent from '@shared/components/dialog/alert-dialog.component'; +import * as TodoDialogComponent from '@shared/components/dialog/todo-dialog.component'; +import * as ColorPickerDialogComponent from '@shared/components/dialog/color-picker-dialog.component'; +import * as MaterialIconsDialogComponent from '@shared/components/dialog/material-icons-dialog.component'; +import * as ColorInputComponent from '@shared/components/color-input.component'; +import * as MaterialIconSelectComponent from '@shared/components/material-icon-select.component'; +import * as NodeScriptTestDialogComponent from '@shared/components/dialog/node-script-test-dialog.component'; +import * as JsonFormComponent from '@shared/components/json-form/json-form.component'; +import * as ImageInputComponent from '@shared/components/image-input.component'; +import * as FileInputComponent from '@shared/components/file-input.component'; +import * as MessageTypeAutocompleteComponent from '@shared/components/message-type-autocomplete.component'; +import * as KeyValMapComponent from '@shared/components/kv-map.component'; +import * as NavTreeComponent from '@shared/components/nav-tree.component'; +import * as LedLightComponent from '@shared/components/led-light.component'; +import * as TbJsonToStringDirective from '@shared/components/directives/tb-json-to-string.directive'; +import * as JsonObjectEditDialogComponent from '@shared/components/dialog/json-object-edit-dialog.component'; +import * as HistorySelectorComponent from '@shared/components/time/history-selector/history-selector.component'; +import * as EntityGatewaySelectComponent from '@shared/components/entity/entity-gateway-select.component'; +import * as ContactComponent from '@shared/components/contact.component'; +import * as OtaPackageAutocompleteComponent from '@shared/components/ota-package/ota-package-autocomplete.component'; +import * as WidgetsBundleSearchComponent from '@shared/components/widgets-bundle-search.component'; +import * as CopyButtonComponent from '@shared/components/button/copy-button.component'; +import * as TogglePasswordComponent from '@shared/components/button/toggle-password.component'; +import * as ProtobufContentComponent from '@shared/components/protobuf-content.component'; + +import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; +import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; +import * as DetailsPanelComponent from '@home/components/details-panel.component'; +import * as EntityDetailsPanelComponent from '@home/components/entity/entity-details-panel.component'; +import * as AuditLogDetailsDialogComponent from '@home/components/audit-log/audit-log-details-dialog.component'; +import * as AuditLogTableComponent from '@home/components/audit-log/audit-log-table.component'; +import * as EventTableHeaderComponent from '@home/components/event/event-table-header.component'; +import * as EventTableComponent from '@home/components/event/event-table.component'; +import * as EventFilterPanelComponent from '@home/components/event/event-filter-panel.component'; +import * as RelationTableComponent from '@home/components/relation/relation-table.component'; +import * as RelationDialogComponent from '@home/components/relation/relation-dialog.component'; +import * as AlarmTableHeaderComponent from '@home/components/alarm/alarm-table-header.component'; +import * as AlarmTableComponent from '@home/components/alarm/alarm-table.component'; +import * as AttributeTableComponent from '@home/components/attribute/attribute-table.component'; +import * as AddAttributeDialogComponent from '@home/components/attribute/add-attribute-dialog.component'; +import * as EditAttributeValuePanelComponent from '@home/components/attribute/edit-attribute-value-panel.component'; +import * as DashboardComponent from '@home/components/dashboard/dashboard.component'; +import * as WidgetComponent from '@home/components/widget/widget.component'; +import * as LegendComponent from '@home/components/widget/legend.component'; +import * as AliasesEntitySelectPanelComponent from '@home/components/alias/aliases-entity-select-panel.component'; +import * as AliasesEntitySelectComponent from '@home/components/alias/aliases-entity-select.component'; +import * as WidgetConfigComponent from '@home/components/widget/widget-config.component'; +import * as EntityAliasesDialogComponent from '@home/components/alias/entity-aliases-dialog.component'; +import * as EntityFilterViewComponent from '@home/components/entity/entity-filter-view.component'; +import * as EntityAliasDialogComponent from '@home/components/alias/entity-alias-dialog.component'; +import * as EntityFilterComponent from '@home/components/entity/entity-filter.component'; +import * as RelationFiltersComponent from '@home/components/relation/relation-filters.component'; +import * as EntityAliasSelectComponent from '@home/components/alias/entity-alias-select.component'; +import * as DataKeysComponent from '@home/components/widget/data-keys.component'; +import * as DataKeyConfigDialogComponent from '@home/components/widget/data-key-config-dialog.component'; +import * as DataKeyConfigComponent from '@home/components/widget/data-key-config.component'; +import * as LegendConfigComponent from '@home/components/widget/legend-config.component'; +import * as ManageWidgetActionsComponent from '@home/components/widget/action/manage-widget-actions.component'; +import * as WidgetActionDialogComponent from '@home/components/widget/action/widget-action-dialog.component'; +import * as CustomActionPrettyResourcesTabsComponent from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; +import * as CustomActionPrettyEditorComponent from '@home/components/widget/action/custom-action-pretty-editor.component'; +import * as MobileActionEditorComponent from '@home/components/widget/action/mobile-action-editor.component'; +import * as CustomDialogService from '@home/components/widget/dialog/custom-dialog.service'; +import * as CustomDialogContainerComponent from '@home/components/widget/dialog/custom-dialog-container.component'; +import * as ImportDialogComponent from '@home/components/import-export/import-dialog.component'; +import * as AddWidgetToDashboardDialogComponent from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; +import * as ImportDialogCsvComponent from '@home/components/import-export/import-dialog-csv.component'; +import * as TableColumnsAssignmentComponent from '@home/components/import-export/table-columns-assignment.component'; +import * as EventContentDialogComponent from '@home/components/event/event-content-dialog.component'; +import * as SharedHomeComponentsModule from '@home/components/shared-home-components.module'; +import * as SelectTargetLayoutDialogComponent from '@home/components/dashboard/select-target-layout-dialog.component'; +import * as SelectTargetStateDialogComponent from '@home/components/dashboard/select-target-state-dialog.component'; +import * as AliasesEntityAutocompleteComponent from '@home/components/alias/aliases-entity-autocomplete.component'; +import * as BooleanFilterPredicateComponent from '@home/components/filter/boolean-filter-predicate.component'; +import * as StringFilterPredicateComponent from '@home/components/filter/string-filter-predicate.component'; +import * as NumericFilterPredicateComponent from '@home/components/filter/numeric-filter-predicate.component'; +import * as ComplexFilterPredicateComponent from '@home/components/filter/complex-filter-predicate.component'; +import * as FilterPredicateComponent from '@home/components/filter/filter-predicate.component'; +import * as FilterPredicateListComponent from '@home/components/filter/filter-predicate-list.component'; +import * as KeyFilterListComponent from '@home/components/filter/key-filter-list.component'; +import * as ComplexFilterPredicateDialogComponent from '@home/components/filter/complex-filter-predicate-dialog.component'; +import * as KeyFilterDialogComponent from '@home/components/filter/key-filter-dialog.component'; +import * as FiltersDialogComponent from '@home/components/filter/filters-dialog.component'; +import * as FilterDialogComponent from '@home/components/filter/filter-dialog.component'; +import * as FilterSelectComponent from '@home/components/filter/filter-select.component'; +import * as FiltersEditComponent from '@home/components/filter/filters-edit.component'; +import * as FiltersEditPanelComponent from '@home/components/filter/filters-edit-panel.component'; +import * as UserFilterDialogComponent from '@home/components/filter/user-filter-dialog.component'; +import * as FilterUserInfoComponent from '@home/components/filter/filter-user-info.component'; +import * as FilterUserInfoDialogComponent from '@home/components/filter/filter-user-info-dialog.component'; +import * as FilterPredicateValueComponent from '@home/components/filter/filter-predicate-value.component'; +import * as TenantProfileComponent from '@home/components/profile/tenant-profile.component'; +import * as TenantProfileDialogComponent from '@home/components/profile/tenant-profile-dialog.component'; +import * as TenantProfileDataComponent from '@home/components/profile/tenant-profile-data.component'; +// tslint:disable-next-line:max-line-length +import * as DefaultDeviceProfileConfigurationComponent from '@home/components/profile/device/default-device-profile-configuration.component'; +import * as DeviceProfileConfigurationComponent from '@home/components/profile/device/device-profile-configuration.component'; +import * as DeviceProfileComponent from '@home/components/profile/device-profile.component'; +import * as DefaultDeviceProfileTransportConfigurationComponent from '@home/components/profile/device/default-device-profile-transport-configuration.component'; +import * as DeviceProfileTransportConfigurationComponent from '@home/components/profile/device/device-profile-transport-configuration.component'; +import * as DeviceProfileDialogComponent from '@home/components/profile/device-profile-dialog.component'; +import * as DeviceProfileAutocompleteComponent from '@home/components/profile/device-profile-autocomplete.component'; +import * as MqttDeviceProfileTransportConfigurationComponent from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component'; +import * as CoapDeviceProfileTransportConfigurationComponent from '@home/components/profile/device/coap-device-profile-transport-configuration.component'; +import * as DeviceProfileAlarmsComponent from '@home/components/profile/alarm/device-profile-alarms.component'; +import * as DeviceProfileAlarmComponent from '@home/components/profile/alarm/device-profile-alarm.component'; +import * as CreateAlarmRulesComponent from '@home/components/profile/alarm/create-alarm-rules.component'; +import * as AlarmRuleComponent from '@home/components/profile/alarm/alarm-rule.component'; +import * as AlarmRuleConditionComponent from '@home/components/profile/alarm/alarm-rule-condition.component'; +import * as FilterTextComponent from '@home/components/filter/filter-text.component'; +import * as AddDeviceProfileDialogComponent from '@home/components/profile/add-device-profile-dialog.component'; +import * as RuleChainAutocompleteComponent from '@home/components/rule-chain/rule-chain-autocomplete.component'; +import * as DeviceProfileProvisionConfigurationComponent from '@home/components/profile/device-profile-provision-configuration.component'; +import * as AlarmScheduleComponent from '@home/components/profile/alarm/alarm-schedule.component'; +import * as DeviceWizardDialogComponent from '@home/components/wizard/device-wizard-dialog.component'; +import * as AlarmScheduleInfoComponent from '@home/components/profile/alarm/alarm-schedule-info.component'; +import * as AlarmScheduleDialogComponent from '@home/components/profile/alarm/alarm-schedule-dialog.component'; +import * as EditAlarmDetailsDialogComponent from '@home/components/profile/alarm/edit-alarm-details-dialog.component'; +import * as AlarmRuleConditionDialogComponent from '@home/components/profile/alarm/alarm-rule-condition-dialog.component'; +// tslint:disable-next-line:max-line-length +import * as DefaultTenantProfileConfigurationComponent from '@home/components/profile/tenant/default-tenant-profile-configuration.component'; +import * as TenantProfileConfigurationComponent from '@home/components/profile/tenant/tenant-profile-configuration.component'; +import * as SmsProviderConfigurationComponent from '@home/components/sms/sms-provider-configuration.component'; +import * as AwsSnsProviderConfigurationComponent from '@home/components/sms/aws-sns-provider-configuration.component'; +import * as TwilioSmsProviderConfigurationComponent from '@home/components/sms/twilio-sms-provider-configuration.component'; +import * as DashboardPageComponent from '@home/components/dashboard-page/dashboard-page.component'; +import * as DashboardToolbarComponent from '@home/components/dashboard-page/dashboard-toolbar.component'; +import * as DashboardLayoutComponent from '@home/components/dashboard-page/layout/dashboard-layout.component'; +import * as EditWidgetComponent from '@home/components/dashboard-page/edit-widget.component'; +import * as DashboardWidgetSelectComponent from '@home/components/dashboard-page/dashboard-widget-select.component'; +import * as AddWidgetDialogComponent from '@home/components/dashboard-page/add-widget-dialog.component'; +import * as ManageDashboardLayoutsDialogComponent from '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component'; +import * as DashboardSettingsDialogComponent from '@home/components/dashboard-page/dashboard-settings-dialog.component'; +import * as ManageDashboardStatesDialogComponent from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component'; +import * as DashboardStateDialogComponent from '@home/components/dashboard-page/states/dashboard-state-dialog.component'; +import * as EmbedDashboardDialogComponent from '@home/components/widget/dialog/embed-dashboard-dialog.component'; +import * as EdgeDownlinkTableComponent from '@home/components/edge/edge-downlink-table.component'; +import * as EdgeDownlinkTableHeaderComponent from '@home/components/edge/edge-downlink-table-header.component'; +import * as DisplayWidgetTypesPanelComponent from '@home/components/dashboard-page/widget-types-panel.component'; +import * as AlarmDurationPredicateValueComponent from '@home/components/profile/alarm/alarm-duration-predicate-value.component'; +import * as DashboardImageDialogComponent from '@home/components/dashboard-page/dashboard-image-dialog.component'; +import * as WidgetContainerComponent from '@home/components/widget/widget-container.component'; + +import { IModulesMap } from '@modules/common/modules-map.models'; + +declare const System; + +class ModulesMap implements IModulesMap { + + private initialized = false; + + private modulesMap: {[key: string]: any} = { + '@angular/animations': AngularAnimations, + '@angular/core': AngularCore, + '@angular/common': AngularCommon, + '@angular/common/http': HttpClientModule, + '@angular/forms': AngularForms, + '@angular/flex-layout': AngularFlexLayout, + '@angular/flex-layout/flex': AngularFlexLayoutFlex, + '@angular/flex-layout/grid': AngularFlexLayoutGrid, + '@angular/flex-layout/extended': AngularFlexLayoutExtended, + '@angular/platform-browser': AngularPlatformBrowser, + '@angular/router': AngularRouter, + '@angular/cdk/coercion': AngularCdkCoercion, + '@angular/cdk/collections': AngularCdkCollections, + '@angular/cdk/keycodes': AngularCdkKeycodes, + '@angular/cdk/layout': AngularCdkLayout, + '@angular/cdk/overlay': AngularCdkOverlay, + '@angular/cdk/portal': AngularCdkPortal, + '@angular/cdk/drag-drop': DragDropModule, + '@angular/material/autocomplete': AngularMaterialAutocomplete, + '@angular/material/badge': AngularMaterialBadge, + '@angular/material/bottom-sheet': AngularMaterialBottomSheet, + '@angular/material/button': AngularMaterialButton, + '@angular/material/button-toggle': AngularMaterialButtonToggle, + '@angular/material/card': AngularMaterialCard, + '@angular/material/checkbox': AngularMaterialCheckbox, + '@angular/material/chips': AngularMaterialChips, + '@angular/material/core': AngularMaterialCore, + '@angular/material/datepicker': AngularMaterialDatepicker, + '@angular/material/dialog': AngularMaterialDialog, + '@angular/material/divider': AngularMaterialDivider, + '@angular/material/expansion': AngularMaterialExpansion, + '@angular/material/form-field': AngularMaterialFormField, + '@angular/material/grid-list': AngularMaterialGridList, + '@angular/material/icon': AngularMaterialIcon, + '@angular/material/input': AngularMaterialInput, + '@angular/material/list': AngularMaterialList, + '@angular/material/menu': AngularMaterialMenu, + '@angular/material/paginator': AngularMaterialPaginator, + '@angular/material/progress-bar': AngularMaterialProgressBar, + '@angular/material/progress-spinner': AngularMaterialProgressSpinner, + '@angular/material/radio': AngularMaterialRadio, + '@angular/material/select': AngularMaterialSelect, + '@angular/material/sidenav': AngularMaterialSidenav, + '@angular/material/slide-toggle': AngularMaterialSlideToggle, + '@angular/material/slider': AngularMaterialSlider, + '@angular/material/snack-bar': AngularMaterialSnackBar, + '@angular/material/sort': AngularMaterialSort, + '@angular/material/stepper': AngularMaterialStepper, + '@angular/material/table': AngularMaterialTable, + '@angular/material/tabs': AngularMaterialTabs, + '@angular/material/toolbar': AngularMaterialToolbar, + '@angular/material/tooltip': AngularMaterialTooltip, + '@angular/material/tree': AngularMaterialTree, + '@ngrx/store': NgrxStore, + rxjs: RxJs, + 'rxjs/operators': RxJsOperators, + '@ngx-translate/core': TranslateCore, + moment: _moment, + + '@core/public-api': TbCore, + '@shared/public-api': TbShared, + '@home/components/public-api': TbHomeComponents, + + '@shared/pipe/milliseconds-to-time-string.pipe': MillisecondsToTimeStringPipe, + '@shared/pipe/enum-to-array.pipe': EnumToArrayPipe, + '@shared/pipe/highlight.pipe': HighlightPipe, + '@shared/pipe/truncate.pipe': TruncatePipe, + '@shared/pipe/tbJson.pipe': TbJsonPipe, + '@shared/pipe/file-size.pipe': FileSizePipe, + '@shared/pipe/nospace.pipe': NospacePipe, + '@shared/pipe/selectable-columns.pipe': SelectableColumnsPipe, + '@shared/pipe/keyboard-shortcut.pipe': KeyboardShortcutPipe, + + '@shared/components/footer.component': FooterComponent, + '@shared/components/logo.component': LogoComponent, + '@shared/components/footer-fab-buttons.component': FooterFabButtonsComponent, + '@shared/components/fullscreen.directive': FullscreenDirective, + '@shared/components/circular-progress.directive': CircularProgressDirective, + '@shared/components/mat-chip-draggable.directive': MatChipDraggableDirective, + '@shared/components/hotkeys.directive': TbHotkeysDirective, + '@shared/components/tb-anchor.component': TbAnchorComponent, + '@shared/components/popover.component': TbPopoverComponent, + '@shared/components/directives/sring-template-outlet.directive': TbStringTemplateOutletDirective, + '@shared/components/directives/component-outlet.directive': TbComponentOutletDirective, + '@shared/components/markdown.component': TbMarkdownComponent, + '@shared/components/help.component': HelpComponent, + '@shared/components/help-markdown.component': HelpMarkdownComponent, + '@shared/components/help-popup.component': HelpPopupComponent, + '@shared/components/tb-checkbox.component': TbCheckboxComponent, + '@shared/components/toast.directive': TbToast, + '@shared/components/tb-error.component': TbErrorComponent, + '@shared/components/cheatsheet.component': TbCheatSheetComponent, + '@shared/components/breadcrumb.component': BreadcrumbComponent, + '@shared/components/user-menu.component': UserMenuComponent, + '@shared/components/time/timewindow.component': TimewindowComponent, + '@shared/components/time/timewindow-panel.component': TimewindowPanelComponent, + '@shared/components/time/timeinterval.component': TimeintervalComponent, + '@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent, + '@shared/components/dashboard-select.component': DashboardSelectComponent, + '@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent, + '@shared/components/time/datetime-period.component': DatetimePeriodComponent, + '@shared/components/time/datetime.component': DatetimeComponent, + '@shared/components/time/timezone-select.component': TimezoneSelectComponent, + '@shared/components/value-input.component': ValueInputComponent, + '@shared/components/dashboard-autocomplete.component': DashboardAutocompleteComponent, + '@shared/components/entity/entity-subtype-autocomplete.component': EntitySubTypeAutocompleteComponent, + '@shared/components/entity/entity-subtype-select.component': EntitySubTypeSelectComponent, + '@shared/components/entity/entity-subtype-list.component': EntitySubTypeListComponent, + '@shared/components/entity/entity-autocomplete.component': EntityAutocompleteComponent, + '@shared/components/entity/entity-list.component': EntityListComponent, + '@shared/components/entity/entity-type-select.component': EntityTypeSelectComponent, + '@shared/components/entity/entity-select.component': EntitySelectComponent, + '@shared/components/entity/entity-keys-list.component': EntityKeysListComponent, + '@shared/components/entity/entity-list-select.component': EntityListSelectComponent, + '@shared/components/entity/entity-type-list.component': EntityTypeListComponent, + '@shared/components/queue/queue-type-list.component': QueueTypeListComponent, + '@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent, + '@shared/components/socialshare-panel.component': SocialSharePanelComponent, + '@shared/components/json-object-edit.component': JsonObjectEditComponent, + '@shared/components/json-content.component': JsonContentComponent, + '@shared/components/js-func.component': JsFuncComponent, + '@shared/components/fab-toolbar.component': FabToolbarComponent, + '@shared/components/widgets-bundle-select.component': WidgetsBundleSelectComponent, + '@shared/components/dialog/confirm-dialog.component': ConfirmDialogComponent, + '@shared/components/dialog/alert-dialog.component': AlertDialogComponent, + '@shared/components/dialog/todo-dialog.component': TodoDialogComponent, + '@shared/components/dialog/color-picker-dialog.component': ColorPickerDialogComponent, + '@shared/components/dialog/material-icons-dialog.component': MaterialIconsDialogComponent, + '@shared/components/color-input.component': ColorInputComponent, + '@shared/components/material-icon-select.component': MaterialIconSelectComponent, + '@shared/components/dialog/node-script-test-dialog.component': NodeScriptTestDialogComponent, + '@shared/components/json-form/json-form.component': JsonFormComponent, + '@shared/components/image-input.component': ImageInputComponent, + '@shared/components/file-input.component': FileInputComponent, + '@shared/components/message-type-autocomplete.component': MessageTypeAutocompleteComponent, + '@shared/components/kv-map.component': KeyValMapComponent, + '@shared/components/nav-tree.component': NavTreeComponent, + '@shared/components/led-light.component': LedLightComponent, + '@shared/components/directives/tb-json-to-string.directive': TbJsonToStringDirective, + '@shared/components/dialog/json-object-edit-dialog.component': JsonObjectEditDialogComponent, + '@shared/components/time/history-selector/history-selector.component': HistorySelectorComponent, + '@shared/components/entity/entity-gateway-select.component': EntityGatewaySelectComponent, + '@shared/components/contact.component': ContactComponent, + '@shared/components/ota-package/ota-package-autocomplete.component': OtaPackageAutocompleteComponent, + '@shared/components/widgets-bundle-search.component': WidgetsBundleSearchComponent, + '@shared/components/button/copy-button.component': CopyButtonComponent, + '@shared/components/button/toggle-password.component': TogglePasswordComponent, + '@shared/components/protobuf-content.component': ProtobufContentComponent, + + '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, + '@home/components/entity/entities-table.component': EntitiesTableComponent, + '@home/components/details-panel.component': DetailsPanelComponent, + '@home/components/entity/entity-details-panel.component': EntityDetailsPanelComponent, + '@home/components/audit-log/audit-log-details-dialog.component': AuditLogDetailsDialogComponent, + '@home/components/audit-log/audit-log-table.component': AuditLogTableComponent, + '@home/components/event/event-table-header.component': EventTableHeaderComponent, + '@home/components/event/event-table.component': EventTableComponent, + '@home/components/event/event-filter-panel.component': EventFilterPanelComponent, + '@home/components/relation/relation-table.component': RelationTableComponent, + '@home/components/relation/relation-dialog.component': RelationDialogComponent, + '@home/components/alarm/alarm-table-header.component': AlarmTableHeaderComponent, + '@home/components/alarm/alarm-table.component': AlarmTableComponent, + '@home/components/attribute/attribute-table.component': AttributeTableComponent, + '@home/components/attribute/add-attribute-dialog.component': AddAttributeDialogComponent, + '@home/components/attribute/edit-attribute-value-panel.component': EditAttributeValuePanelComponent, + '@home/components/dashboard/dashboard.component': DashboardComponent, + '@home/components/widget/widget.component': WidgetComponent, + '@home/components/widget/legend.component': LegendComponent, + '@home/components/alias/aliases-entity-select-panel.component': AliasesEntitySelectPanelComponent, + '@home/components/alias/aliases-entity-select.component': AliasesEntitySelectComponent, + '@home/components/widget/widget-config.component': WidgetConfigComponent, + '@home/components/alias/entity-aliases-dialog.component': EntityAliasesDialogComponent, + '@home/components/entity/entity-filter-view.component': EntityFilterViewComponent, + '@home/components/alias/entity-alias-dialog.component': EntityAliasDialogComponent, + '@home/components/entity/entity-filter.component': EntityFilterComponent, + '@home/components/relation/relation-filters.component': RelationFiltersComponent, + '@home/components/alias/entity-alias-select.component': EntityAliasSelectComponent, + '@home/components/widget/data-keys.component': DataKeysComponent, + '@home/components/widget/data-key-config-dialog.component': DataKeyConfigDialogComponent, + '@home/components/widget/data-key-config.component': DataKeyConfigComponent, + '@home/components/widget/legend-config.component': LegendConfigComponent, + '@home/components/widget/action/manage-widget-actions.component': ManageWidgetActionsComponent, + '@home/components/widget/action/widget-action-dialog.component': WidgetActionDialogComponent, + '@home/components/widget/action/custom-action-pretty-resources-tabs.component': CustomActionPrettyResourcesTabsComponent, + '@home/components/widget/action/custom-action-pretty-editor.component': CustomActionPrettyEditorComponent, + '@home/components/widget/action/mobile-action-editor.component': MobileActionEditorComponent, + '@home/components/widget/dialog/custom-dialog.service': CustomDialogService, + '@home/components/widget/dialog/custom-dialog-container.component': CustomDialogContainerComponent, + '@home/components/import-export/import-dialog.component': ImportDialogComponent, + '@home/components/attribute/add-widget-to-dashboard-dialog.component': AddWidgetToDashboardDialogComponent, + '@home/components/import-export/import-dialog-csv.component': ImportDialogCsvComponent, + '@home/components/import-export/table-columns-assignment.component': TableColumnsAssignmentComponent, + '@home/components/event/event-content-dialog.component': EventContentDialogComponent, + '@home/components/shared-home-components.module': SharedHomeComponentsModule, + '@home/components/dashboard/select-target-layout-dialog.component': SelectTargetLayoutDialogComponent, + '@home/components/dashboard/select-target-state-dialog.component': SelectTargetStateDialogComponent, + '@home/components/alias/aliases-entity-autocomplete.component': AliasesEntityAutocompleteComponent, + '@home/components/filter/boolean-filter-predicate.component': BooleanFilterPredicateComponent, + '@home/components/filter/string-filter-predicate.component': StringFilterPredicateComponent, + '@home/components/filter/numeric-filter-predicate.component': NumericFilterPredicateComponent, + '@home/components/filter/complex-filter-predicate.component': ComplexFilterPredicateComponent, + '@home/components/filter/filter-predicate.component': FilterPredicateComponent, + '@home/components/filter/filter-predicate-list.component': FilterPredicateListComponent, + '@home/components/filter/key-filter-list.component': KeyFilterListComponent, + '@home/components/filter/complex-filter-predicate-dialog.component': ComplexFilterPredicateDialogComponent, + '@home/components/filter/key-filter-dialog.component': KeyFilterDialogComponent, + '@home/components/filter/filters-dialog.component': FiltersDialogComponent, + '@home/components/filter/filter-dialog.component': FilterDialogComponent, + '@home/components/filter/filter-select.component': FilterSelectComponent, + '@home/components/filter/filters-edit.component': FiltersEditComponent, + '@home/components/filter/filters-edit-panel.component': FiltersEditPanelComponent, + '@home/components/filter/user-filter-dialog.component': UserFilterDialogComponent, + '@home/components/filter/filter-user-info.component': FilterUserInfoComponent, + '@home/components/filter/filter-user-info-dialog.component': FilterUserInfoDialogComponent, + '@home/components/filter/filter-predicate-value.component': FilterPredicateValueComponent, + '@home/components/profile/tenant-profile.component': TenantProfileComponent, + '@home/components/profile/tenant-profile-dialog.component': TenantProfileDialogComponent, + '@home/components/profile/tenant-profile-data.component': TenantProfileDataComponent, + '@home/components/profile/device/default-device-profile-configuration.component': DefaultDeviceProfileConfigurationComponent, + '@home/components/profile/device/device-profile-configuration.component': DeviceProfileConfigurationComponent, + '@home/components/profile/device-profile.component': DeviceProfileComponent, + '@home/components/profile/device/default-device-profile-transport-configuration.component': + DefaultDeviceProfileTransportConfigurationComponent, + '@home/components/profile/device/device-profile-transport-configuration.component': DeviceProfileTransportConfigurationComponent, + '@home/components/profile/device-profile-dialog.component': DeviceProfileDialogComponent, + '@home/components/profile/device-profile-autocomplete.component': DeviceProfileAutocompleteComponent, + '@home/components/profile/device/mqtt-device-profile-transport-configuration.component': + MqttDeviceProfileTransportConfigurationComponent, + '@home/components/profile/device/coap-device-profile-transport-configuration.component': + CoapDeviceProfileTransportConfigurationComponent, + '@home/components/profile/alarm/device-profile-alarms.component': DeviceProfileAlarmsComponent, + '@home/components/profile/alarm/device-profile-alarm.component': DeviceProfileAlarmComponent, + '@home/components/profile/alarm/create-alarm-rules.component': CreateAlarmRulesComponent, + '@home/components/profile/alarm/alarm-rule.component': AlarmRuleComponent, + '@home/components/profile/alarm/alarm-rule-condition.component': AlarmRuleConditionComponent, + '@home/components/filter/filter-text.component': FilterTextComponent, + '@home/components/profile/add-device-profile-dialog.component': AddDeviceProfileDialogComponent, + '@home/components/rule-chain/rule-chain-autocomplete.component': RuleChainAutocompleteComponent, + '@home/components/profile/device-profile-provision-configuration.component': DeviceProfileProvisionConfigurationComponent, + '@home/components/profile/alarm/alarm-schedule.component': AlarmScheduleComponent, + '@home/components/wizard/device-wizard-dialog.component': DeviceWizardDialogComponent, + '@home/components/profile/alarm/alarm-schedule-info.component': AlarmScheduleInfoComponent, + '@home/components/profile/alarm/alarm-schedule-dialog.component': AlarmScheduleDialogComponent, + '@home/components/profile/alarm/edit-alarm-details-dialog.component': EditAlarmDetailsDialogComponent, + '@home/components/profile/alarm/alarm-rule-condition-dialog.component': AlarmRuleConditionDialogComponent, + '@home/components/profile/tenant/default-tenant-profile-configuration.component': DefaultTenantProfileConfigurationComponent, + '@home/components/profile/tenant/tenant-profile-configuration.component': TenantProfileConfigurationComponent, + '@home/components/sms/sms-provider-configuration.component': SmsProviderConfigurationComponent, + '@home/components/sms/aws-sns-provider-configuration.component': AwsSnsProviderConfigurationComponent, + '@home/components/sms/twilio-sms-provider-configuration.component': TwilioSmsProviderConfigurationComponent, + '@home/components/dashboard-page/dashboard-page.component': DashboardPageComponent, + '@home/components/dashboard-page/dashboard-toolbar.component': DashboardToolbarComponent, + '@home/components/dashboard-page/layout/dashboard-layout.component': DashboardLayoutComponent, + '@home/components/dashboard-page/edit-widget.component': EditWidgetComponent, + '@home/components/dashboard-page/dashboard-widget-select.component': DashboardWidgetSelectComponent, + '@home/components/dashboard-page/add-widget-dialog.component': AddWidgetDialogComponent, + '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component': ManageDashboardLayoutsDialogComponent, + '@home/components/dashboard-page/dashboard-settings-dialog.component': DashboardSettingsDialogComponent, + '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component': ManageDashboardStatesDialogComponent, + '@home/components/dashboard-page/states/dashboard-state-dialog.component': DashboardStateDialogComponent, + '@home/components/widget/dialog/embed-dashboard-dialog.component': EmbedDashboardDialogComponent, + '@home/components/edge/edge-downlink-table.component': EdgeDownlinkTableComponent, + '@home/components/edge/edge-downlink-table-header.component': EdgeDownlinkTableHeaderComponent, + '@home/components/dashboard-page/widget-types-panel.component': DisplayWidgetTypesPanelComponent, + '@home/components/profile/alarm/alarm-duration-predicate-value.component': AlarmDurationPredicateValueComponent, + '@home/components/dashboard-page/dashboard-image-dialog.component': DashboardImageDialogComponent, + '@home/components/widget/widget-container.component': WidgetContainerComponent, + }; + + init() { + if (!this.initialized) { + System.constructor.prototype.resolve = (id) => { + try { + if (this.modulesMap[id]) { + return 'app:' + id; + } else { + return id; + } + } catch (err) { + return id; + } + }; + for (const moduleId of Object.keys(this.modulesMap)) { + System.set('app:' + moduleId, this.modulesMap[moduleId]); + } + this.initialized = true; + } + } +} + +export const modulesMap = new ModulesMap(); 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/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 923589ed39..f64b63fe9f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -36,6 +36,9 @@ {{ 'attribute.key-required' | translate }} + + {{ 'attribute.key-max-length' | translate }} + -
-

{{ dashboard.title }}

+

{{ translatedDashboardTitle }}

dashboard.title { - if (paramMap.has('reload')) { - this.dashboardCtx.aliasController.updateAliases(); - setTimeout(() => { - this.mobileService.handleDashboardStateName(this.dashboardCtx.stateController.getCurrentStateName()); - this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened); - }); + if (this.syncStateWithQueryParam) { + this.rxSubscriptions.push(this.route.queryParamMap.subscribe( + (paramMap) => { + if (paramMap.has('reload')) { + this.dashboardCtx.aliasController.updateAliases(); + setTimeout(() => { + this.mobileService.handleDashboardStateName(this.dashboardCtx.stateController.getCurrentStateName()); + this.mobileService.onDashboardLoaded(this.layouts.right.show, this.isRightLayoutOpened); + }); + } } - } - )); + )); + } this.rxSubscriptions.push(this.breakpointObserver .observe(MediaBreakpoints['gt-sm']) .subscribe((state: BreakpointState) => { this.isMobile = !state.matches; } )); - if (this.isMobileApp) { + if (this.isMobileApp && this.syncStateWithQueryParam) { this.mobileService.registerToggleLayoutFunction(() => { setTimeout(() => { this.toggleLayouts(); @@ -375,6 +379,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC this.reset(); this.dashboard = data.dashboard; + this.translatedDashboardTitle = this.getTranslatedDashboardTitle(); if (!this.embedded && this.dashboard.id) { this.setStateDashboardId = true; } @@ -424,6 +429,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC private reset() { this.dashboard = null; + this.translatedDashboardTitle = null; this.dashboardConfiguration = null; this.dashboardLogoCache = undefined; this.prevDashboard = null; @@ -460,7 +466,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } ngOnDestroy(): void { - if (this.isMobileApp) { + if (this.isMobileApp && this.syncStateWithQueryParam) { this.mobileService.unregisterToggleLayoutFunction(); } this.rxSubscriptions.forEach((subscription) => { @@ -520,6 +526,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } } + private getTranslatedDashboardTitle(): string { + return this.utils.customTranslation(this.dashboard.title, this.dashboard.title); + } + public displayExport(): boolean { if (this.dashboard.configuration.settings && isDefined(this.dashboard.configuration.settings.showDashboardExport)) { @@ -817,6 +827,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } public saveDashboard() { + this.translatedDashboardTitle = this.getTranslatedDashboardTitle(); this.setEditMode(false, false); this.notifyDashboardUpdated(); } @@ -873,6 +884,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC if (revert) { this.dashboard = this.prevDashboard; this.dashboardLogoCache = undefined; + this.dashboardConfiguration = this.dashboard.configuration; } } else { this.resetHighlight(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-controller.module.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-controller.module.ts index 0f8c60858a..66b14fee44 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-controller.module.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/states-controller.module.ts @@ -17,9 +17,9 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { StatesControllerService } from './states-controller.service'; -import { EntityStateControllerComponent } from './entity-state-controller.component'; -import { StatesComponentDirective } from './states-component.directive'; +import { StatesControllerService } from '@home/components/dashboard-page/states/states-controller.service'; +import { EntityStateControllerComponent } from '@home/components/dashboard-page/states/entity-state-controller.component'; +import { StatesComponentDirective } from '@home/components/dashboard-page/states/states-component.directive'; import { HomeDialogsModule } from '@app/modules/home/dialogs/home-dialogs.module'; import { DefaultStateControllerComponent } from '@home/components/dashboard-page/states/default-state-controller.component'; 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 348d084772..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 @@ -242,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/device/device-credentials.module.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts index 49f1c2c097..ce3bbbbd97 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts @@ -17,11 +17,11 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; -import { CopyDeviceCredentialsComponent } from './copy-device-credentials.component'; -import { DeviceCredentialsComponent } from './device-credentials.component'; -import { DeviceCredentialsLwm2mComponent } from './device-credentials-lwm2m.component'; -import { DeviceCredentialsLwm2mServerComponent } from './device-credentials-lwm2m-server.component'; -import { DeviceCredentialsMqttBasicComponent } from './device-credentials-mqtt-basic.component'; +import { CopyDeviceCredentialsComponent } from '@home/components/device/copy-device-credentials.component'; +import { DeviceCredentialsComponent } from '@home/components/device/device-credentials.component'; +import { DeviceCredentialsLwm2mComponent } from '@home/components/device/device-credentials-lwm2m.component'; +import { DeviceCredentialsLwm2mServerComponent } from '@home/components/device/device-credentials-lwm2m-server.component'; +import { DeviceCredentialsMqttBasicComponent } from '@home/components/device/device-credentials-mqtt-basic.component'; @NgModule({ declarations: [ diff --git a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts index 98c856eb9d..f005bcc283 100644 --- a/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/contact-based.component.ts @@ -37,15 +37,15 @@ export abstract class ContactBasedComponent> exten buildForm(entity: T): FormGroup { const entityForm = this.buildEntityForm(entity); - entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [])); - entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [])); - entityForm.addControl('state', this.fb.control(entity ? entity.state : '', [])); + entityForm.addControl('country', this.fb.control(entity ? entity.country : '', [Validators.maxLength(255)])); + entityForm.addControl('city', this.fb.control(entity ? entity.city : '', [Validators.maxLength(255)])); + entityForm.addControl('state', this.fb.control(entity ? entity.state : '', [Validators.maxLength(255)])); entityForm.addControl('zip', this.fb.control(entity ? entity.zip : '', this.zipValidators(entity ? entity.country : '') )); entityForm.addControl('address', this.fb.control(entity ? entity.address : '', [])); entityForm.addControl('address2', this.fb.control(entity ? entity.address2 : '', [])); - entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [])); + entityForm.addControl('phone', this.fb.control(entity ? entity.phone : '', [Validators.maxLength(255)])); entityForm.addControl('email', this.fb.control(entity ? entity.email : '', [Validators.email])); return entityForm; } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 84c421c5c1..33f1b2afe4 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -64,6 +64,7 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; @Component({ selector: 'tb-entities-table', @@ -71,7 +72,7 @@ import { HasUUID } from '@shared/models/id/has-uuid'; styleUrls: ['./entities-table.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class EntitiesTableComponent extends PageComponent implements AfterViewInit, OnInit, OnChanges { +export class EntitiesTableComponent extends PageComponent implements IEntitiesTableComponent, AfterViewInit, OnInit, OnChanges { @Input() entitiesTableConfig: EntityTableConfig>; @@ -432,6 +433,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn } onTimewindowChange() { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } this.updateData(); } diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html index 579301d086..98d83e3c79 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html @@ -44,10 +44,10 @@ {{ 'event.has-error' | translate }} - + {{ 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/complex-filter-predicate-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts index 3f1bb3cace..4d61aa2c89 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.ts @@ -23,22 +23,12 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida import { Router } from '@angular/router'; import { DialogComponent } from '@app/shared/components/dialog.component'; import { - BooleanOperation, booleanOperationTranslationMap, - ComplexFilterPredicate, ComplexFilterPredicateInfo, ComplexOperation, complexOperationTranslationMap, - EntityKeyValueType, - FilterPredicateType, KeyFilterPredicateInfo + ComplexFilterPredicateInfo, + ComplexOperation, + complexOperationTranslationMap, + FilterPredicateType } from '@shared/models/query/query.models'; - -export interface ComplexFilterPredicateDialogData { - complexPredicate: ComplexFilterPredicateInfo; - key: string; - readonly: boolean; - isAdd: boolean; - valueType: EntityKeyValueType; - displayUserParameters: boolean; - allowUserDynamicSource: boolean; - onlyUserDynamicSource: boolean; -} +import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; @Component({ selector: 'tb-complex-filter-predicate-dialog', diff --git a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts index b177f118c0..038ff474ae 100644 --- a/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts @@ -14,15 +14,14 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Inject, Input, OnInit } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models'; import { MatDialog } from '@angular/material/dialog'; -import { - ComplexFilterPredicateDialogComponent, - ComplexFilterPredicateDialogData -} from '@home/components/filter/complex-filter-predicate-dialog.component'; import { deepClone } from '@core/utils'; +import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; +import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens'; +import { ComponentType } from '@angular/cdk/portal'; @Component({ selector: 'tb-complex-filter-predicate', @@ -54,7 +53,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On private complexFilterPredicate: ComplexFilterPredicateInfo; - constructor(private dialog: MatDialog) { + constructor(@Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType, + private dialog: MatDialog) { } ngOnInit(): void { @@ -76,8 +76,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On } public openComplexFilterDialog() { - this.dialog.open(ComplexFilterPredicateDialogComponent, { + this.dialog.open(this.complexFilterPredicateDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-component.models.ts b/ui-ngx/src/app/modules/home/components/filter/filter-component.models.ts new file mode 100644 index 0000000000..325f89a911 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/filter/filter-component.models.ts @@ -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. +/// + +import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models'; + +export interface ComplexFilterPredicateDialogData { + complexPredicate: ComplexFilterPredicateInfo; + key: string; + readonly: boolean; + isAdd: boolean; + valueType: EntityKeyValueType; + displayUserParameters: boolean; + allowUserDynamicSource: boolean; + onlyUserDynamicSource: boolean; +} + diff --git a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts index 75c9bd3ce8..e23c135d9b 100644 --- a/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { Component, forwardRef, Inject, Input, OnInit } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -36,12 +36,11 @@ import { EntityKeyValueType, KeyFilterPredicateInfo } from '@shared/models/query/query.models'; -import { - ComplexFilterPredicateDialogComponent, - ComplexFilterPredicateDialogData -} from '@home/components/filter/complex-filter-predicate-dialog.component'; import { MatDialog } from '@angular/material/dialog'; import { map } from 'rxjs/operators'; +import { ComponentType } from '@angular/cdk/portal'; +import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens'; +import { ComplexFilterPredicateDialogData } from '@home/components/filter/filter-component.models'; @Component({ selector: 'tb-filter-predicate-list', @@ -87,6 +86,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, Valid private valueChangeSubscription: Subscription = null; constructor(private fb: FormBuilder, + @Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType, private dialog: MatDialog) { } @@ -164,8 +164,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, Valid } private openComplexFilterDialog(predicate: KeyFilterPredicateInfo): Observable { - return this.dialog.open(ComplexFilterPredicateDialogComponent, { + return this.dialog.open(this.complexFilterPredicateDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 71b983b4d4..265c5eb659 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -49,7 +49,6 @@ import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias- import { DataKeysComponent } from '@home/components/widget/data-keys.component'; import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; -import { LegendConfigPanelComponent } from '@home/components/widget/legend-config-panel.component'; import { LegendConfigComponent } from '@home/components/widget/legend-config.component'; import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; @@ -142,6 +141,10 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module'; import { DeviceCredentialsModule } from '@home/components/device/device-credentials.module'; import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module'; +import { + COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, + DASHBOARD_PAGE_COMPONENT_TOKEN +} from '@home/components/tokens'; @NgModule({ declarations: @@ -182,7 +185,6 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo DataKeysComponent, DataKeyConfigComponent, DataKeyConfigDialogComponent, - LegendConfigPanelComponent, LegendConfigComponent, ManageWidgetActionsComponent, WidgetActionDialogComponent, @@ -377,7 +379,9 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo WidgetComponentService, CustomDialogService, ImportExportService, - {provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent} + {provide: EMBED_DASHBOARD_DIALOG_TOKEN, useValue: EmbedDashboardDialogComponent}, + {provide: COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN, useValue: ComplexFilterPredicateDialogComponent}, + {provide: DASHBOARD_PAGE_COMPONENT_TOKEN, useValue: DashboardPageComponent} ] }) export class HomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index 61cc7ae478..20e21f0aad 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -423,7 +423,7 @@ export class ImportExportService { public importRuleChain(expectedRuleChainType: RuleChainType): Observable { return this.openImportDialog('rulechain.import', 'rulechain.rulechain-file').pipe( - mergeMap((ruleChainImport: RuleChainImport) => { + map((ruleChainImport: RuleChainImport) => { if (!this.validateImportedRuleChain(ruleChainImport)) { this.store.dispatch(new ActionNotificationShow( {message: this.translate.instant('rulechain.invalid-rulechain-file-error'), @@ -435,12 +435,7 @@ export class ImportExportService { type: 'error'})); throw new Error('Invalid rule chain type'); } else { - return this.ruleChainService.resolveRuleChainMetadata(ruleChainImport.metadata).pipe( - map((resolvedMetadata) => { - ruleChainImport.resolvedMetadata = resolvedMetadata; - return ruleChainImport; - }) - ); + return ruleChainImport; } }), catchError((err) => { diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html index b91613955d..23fdf04cde 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html @@ -41,6 +41,9 @@ {{ 'device-profile.name-required' | translate }} + + {{ 'device-profile.name-max-length' | translate }} + {{ 'device-profile.alarm-type-unique' | translate }} + + {{ 'device-profile.alarm-type-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts index a77641505b..20ea89e53c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -93,7 +93,7 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit ngOnInit() { this.alarmFormGroup = this.fb.group({ id: [null, Validators.required], - alarmType: [null, Validators.required], + alarmType: [null, [Validators.required, Validators.maxLength(255)]], createRules: [null], clearRule: [null], propagate: [null], diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 83bba2a266..917ad25dd2 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -54,6 +54,9 @@ {{ 'device-profile.name-required' | translate }} + + {{ 'device-profile.name-max-length' | translate }} + { }; const form = this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], type: [entity ? entity.type : null, [Validators.required]], image: [entity ? entity.image : null], transportType: [entity ? entity.transportType : null, [Validators.required]], diff --git a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html index 8e1680ea46..24b6ff099b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html @@ -47,35 +47,50 @@
- - device-profile.telemetry-proto-schema - + + + {{ 'device-profile.telemetry-proto-schema-required' | translate}} - - - device-profile.attributes-proto-schema - + + + + {{ 'device-profile.attributes-proto-schema-required' | translate}} - - - device-profile.rpc-request-proto-schema - + + + + {{ 'device-profile.rpc-request-proto-schema-required' | translate}} - device-profile.rpc-request-proto-schema-hint - - - device-profile.rpc-response-proto-schema - + + + + {{ 'device-profile.rpc-response-proto-schema-required' | translate}} - +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/common/device-profile-common.module.ts b/ui-ngx/src/app/modules/home/components/profile/device/common/device-profile-common.module.ts index 0ddcb507bb..e4bbdc919f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/common/device-profile-common.module.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/common/device-profile-common.module.ts @@ -15,10 +15,10 @@ /// import { NgModule } from '@angular/core'; -import { PowerModeSettingComponent } from './power-mode-setting.component'; +import { PowerModeSettingComponent } from '@home/components/profile/device/common/power-mode-setting.component'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; -import { TimeUnitSelectComponent } from './time-unit-select.component'; +import { TimeUnitSelectComponent } from '@home/components/profile/device/common/time-unit-select.component'; @NgModule({ declarations: [ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts index aa801f84dc..e1c8e9a14c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-components.module.ts @@ -15,19 +15,19 @@ /// import { NgModule } from '@angular/core'; -import { Lwm2mDeviceProfileTransportConfigurationComponent } from './lwm2m-device-profile-transport-configuration.component'; -import { Lwm2mObjectListComponent } from './lwm2m-object-list.component'; -import { Lwm2mObserveAttrTelemetryComponent } from './lwm2m-observe-attr-telemetry.component'; -import { Lwm2mObserveAttrTelemetryResourcesComponent } from './lwm2m-observe-attr-telemetry-resources.component'; -import { Lwm2mAttributesDialogComponent } from './lwm2m-attributes-dialog.component'; -import { Lwm2mAttributesComponent } from './lwm2m-attributes.component'; -import { Lwm2mAttributesKeyListComponent } from './lwm2m-attributes-key-list.component'; -import { Lwm2mDeviceConfigServerComponent } from './lwm2m-device-config-server.component'; -import { Lwm2mObjectAddInstancesDialogComponent } from './lwm2m-object-add-instances-dialog.component'; -import { Lwm2mObjectAddInstancesListComponent } from './lwm2m-object-add-instances-list.component'; +import { Lwm2mDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component'; +import { Lwm2mObjectListComponent } from '@home/components/profile/device/lwm2m/lwm2m-object-list.component'; +import { Lwm2mObserveAttrTelemetryComponent } from '@home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry.component'; +import { Lwm2mObserveAttrTelemetryResourcesComponent } from '@home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component'; +import { Lwm2mAttributesDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component'; +import { Lwm2mAttributesComponent } from '@home/components/profile/device/lwm2m/lwm2m-attributes.component'; +import { Lwm2mAttributesKeyListComponent } from '@home/components/profile/device/lwm2m/lwm2m-attributes-key-list.component'; +import { Lwm2mDeviceConfigServerComponent } from '@home/components/profile/device/lwm2m/lwm2m-device-config-server.component'; +import { Lwm2mObjectAddInstancesDialogComponent } from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances-dialog.component'; +import { Lwm2mObjectAddInstancesListComponent } from '@home/components/profile/device/lwm2m/lwm2m-object-add-instances-list.component'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@app/shared/shared.module'; -import { Lwm2mObserveAttrTelemetryInstancesComponent } from './lwm2m-observe-attr-telemetry-instances.component'; +import { Lwm2mObserveAttrTelemetryInstancesComponent } from '@home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component'; import { DeviceProfileCommonModule } from '@home/components/profile/device/common/device-profile-common.module'; @NgModule({ diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html index 55f0869dbc..5b597a1d47 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -73,36 +73,63 @@ {{ 'device-profile.mqtt-payload-type-required' | translate }} +
+ + {{ 'device-profile.mqtt-enable-compatibility-with-json-payload-format' | translate }} + +
+
+ + {{ 'device-profile.mqtt-use-json-format-for-default-downlink-topics' | translate }} + +
+
+
- - device-profile.telemetry-proto-schema - + + + {{ 'device-profile.telemetry-proto-schema-required' | translate}} - - - device-profile.attributes-proto-schema - + + + + {{ 'device-profile.attributes-proto-schema-required' | translate}} - - - device-profile.rpc-request-proto-schema - + + + + {{ 'device-profile.rpc-request-proto-schema-required' | translate}} - device-profile.rpc-request-proto-schema-hint - - - device-profile.rpc-response-proto-schema - + + + + {{ 'device-profile.rpc-response-proto-schema-required' | translate}} - +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss index d1a270602e..4e464f5e49 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.scss @@ -24,8 +24,9 @@ color: rgba(0, 0, 0, .7); } - .tb-hint{ + .tb-hint { padding: 0; + max-width: fit-content; } } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts index 5254b7574f..61f72f6c60 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -97,7 +97,9 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control deviceTelemetryProtoSchema: [defaultTelemetrySchema, Validators.required], deviceAttributesProtoSchema: [defaultAttributesSchema, Validators.required], deviceRpcRequestProtoSchema: [defaultRpcRequestSchema, Validators.required], - deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] + deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required], + enableCompatibilityWithJsonPayloadFormat: [false, Validators.required], + useJsonPayloadFormatForDefaultDownlinkTopics: [false, Validators.required] }) }, {validator: this.uniqueDeviceTopicValidator} ); @@ -106,6 +108,14 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control ).subscribe(payloadType => { this.updateTransportPayloadBasedControls(payloadType, true); }); + this.mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.enableCompatibilityWithJsonPayloadFormat') + .valueChanges.pipe(takeUntil(this.destroy$) + ).subscribe(compatibilityWithJsonPayloadFormatEnabled => { + if (!compatibilityWithJsonPayloadFormatEnabled) { + this.mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.useJsonPayloadFormatForDefaultDownlinkTopics') + .patchValue(false, {emitEvent: false}); + } + }); this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { @@ -132,6 +142,10 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control return transportPayloadType === TransportPayloadType.PROTOBUF; } + get compatibilityWithJsonPayloadFormatEnabled(): boolean { + return this.mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.enableCompatibilityWithJsonPayloadFormat').value; + } + writeValue(value: MqttDeviceProfileTransportConfiguration | null): void { if (isDefinedAndNotNull(value)) { this.mqttDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: false}); @@ -156,7 +170,9 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control deviceTelemetryProtoSchema: defaultTelemetrySchema, deviceAttributesProtoSchema: defaultAttributesSchema, deviceRpcRequestProtoSchema: defaultRpcRequestSchema, - deviceRpcResponseProtoSchema: defaultRpcResponseSchema + deviceRpcResponseProtoSchema: defaultRpcResponseSchema, + enableCompatibilityWithJsonPayloadFormat: false, + useJsonPayloadFormatForDefaultDownlinkTopics: false }, {emitEvent: false}); } if (type === TransportPayloadType.PROTOBUF && !this.disabled) { @@ -164,11 +180,15 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control transportPayloadTypeForm.get('deviceAttributesProtoSchema').enable({emitEvent: false}); transportPayloadTypeForm.get('deviceRpcRequestProtoSchema').enable({emitEvent: false}); transportPayloadTypeForm.get('deviceRpcResponseProtoSchema').enable({emitEvent: false}); + transportPayloadTypeForm.get('enableCompatibilityWithJsonPayloadFormat').enable({emitEvent: false}); + transportPayloadTypeForm.get('useJsonPayloadFormatForDefaultDownlinkTopics').enable({emitEvent: false}); } else { transportPayloadTypeForm.get('deviceTelemetryProtoSchema').disable({emitEvent: false}); transportPayloadTypeForm.get('deviceAttributesProtoSchema').disable({emitEvent: false}); transportPayloadTypeForm.get('deviceRpcRequestProtoSchema').disable({emitEvent: false}); transportPayloadTypeForm.get('deviceRpcResponseProtoSchema').disable({emitEvent: false}); + transportPayloadTypeForm.get('enableCompatibilityWithJsonPayloadFormat').disable({emitEvent: false}); + transportPayloadTypeForm.get('useJsonPayloadFormatForDefaultDownlinkTopics').disable({emitEvent: false}); } } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts b/ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts index c0797d9b0e..95b7c0180d 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts @@ -18,8 +18,8 @@ import { NgModule } from '@angular/core'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component'; -import { SnmpDeviceProfileCommunicationConfigComponent } from './snmp-device-profile-communication-config.component'; -import { SnmpDeviceProfileMappingComponent } from './snmp-device-profile-mapping.component'; +import { SnmpDeviceProfileCommunicationConfigComponent } from '@home/components/profile/device/snpm/snmp-device-profile-communication-config.component'; +import { SnmpDeviceProfileMappingComponent } from '@home/components/profile/device/snpm/snmp-device-profile-mapping.component'; @NgModule({ declarations: [ diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index e1dd51d1cd..e5a3a54293 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -48,6 +48,9 @@ {{ 'tenant-profile.name-required' | translate }} + + {{ 'tenant-profile.name-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts index f8685cc193..24636b565c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.ts @@ -59,7 +59,7 @@ export class TenantProfileComponent extends EntityComponent { buildForm(entity: TenantProfile): FormGroup { return this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], profileData: [entity && !this.isAdd ? entity.profileData : { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index cd88d9fae0..b10ed82249 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -124,6 +124,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn directionChanged(direction: EntitySearchDirection) { this.direction = direction; this.updateColumns(); + this.paginator.pageIndex = 0; this.updateData(true); } diff --git a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts index df6764c573..f2c14a535a 100644 --- a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts @@ -18,8 +18,12 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@app/shared/shared.module'; import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component'; +import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; @NgModule({ + providers: [ + { provide: SHARED_HOME_COMPONENTS_MODULE_TOKEN, useValue: SharedHomeComponentsModule } + ], declarations: [ AlarmDetailsDialogComponent diff --git a/ui-ngx/src/app/modules/home/components/tokens.ts b/ui-ngx/src/app/modules/home/components/tokens.ts new file mode 100644 index 0000000000..b88ea49a03 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/tokens.ts @@ -0,0 +1,27 @@ +/// +/// 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 { InjectionToken, Type } from '@angular/core'; +import { ComponentType } from '@angular/cdk/portal'; + +export const SHARED_HOME_COMPONENTS_MODULE_TOKEN: InjectionToken> = + new InjectionToken>('SHARED_HOME_COMPONENTS_MODULE_TOKEN'); + +export const COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN: InjectionToken> = + new InjectionToken>('COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN'); + +export const DASHBOARD_PAGE_COMPONENT_TOKEN: InjectionToken> = + new InjectionToken>('DASHBOARD_PAGE_COMPONENT_TOKEN'); 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 401dfb8bd7..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 @@ -37,6 +37,7 @@ [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_get_location_fn" > @@ -46,6 +47,7 @@ [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_get_phone_number_fn" > @@ -67,6 +70,7 @@ [functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_process_qr_code_fn" > @@ -76,6 +80,7 @@ [functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" [globalVariables]="functionScopeVariables" [editorCompleter]="customActionEditorCompleter" + helpId="widget/action/mobile_process_location_fn" >
@@ -97,6 +103,7 @@ [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.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/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html index 4bbfb097bc..96c47dda92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ 'datakey.configuration' | translate }}

diff --git a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts index ac41188485..a2d61a9eaa 100644 --- a/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts @@ -14,21 +14,21 @@ /// limitations under the License. /// -import { Injectable, NgModule } from '@angular/core'; +import { Inject, Injectable, Type } from '@angular/core'; import { Observable } from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { TranslateService } from '@ngx-translate/core'; import { AuthService } from '@core/auth/auth.service'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { CommonModule } from '@angular/common'; -import { SharedModule } from '@shared/shared.module'; import { mergeMap, tap } from 'rxjs/operators'; import { CustomDialogComponent } from './custom-dialog.component'; import { CustomDialogContainerComponent, CustomDialogContainerData } from '@home/components/widget/dialog/custom-dialog-container.component'; -import { SharedHomeComponentsModule } from '@home/components/shared-home-components.module'; +import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; +import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; @Injectable() export class CustomDialogService { @@ -37,6 +37,8 @@ export class CustomDialogService { private translate: TranslateService, private authService: AuthService, private dynamicComponentFactoryService: DynamicComponentFactoryService, + @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type, + @Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type, public dialog: MatDialog ) { } @@ -45,7 +47,7 @@ export class CustomDialogService { return this.dynamicComponentFactoryService.createDynamicComponentFactory( class CustomDialogComponentInstance extends CustomDialogComponent {}, template, - [SharedModule, CustomDialogModule, SharedHomeComponentsModule]).pipe( + [this.sharedModule, CommonModule, this.sharedHomeComponentsModule]).pipe( mergeMap((factory) => { const dialogData: CustomDialogContainerData = { controller, @@ -69,15 +71,3 @@ export class CustomDialogService { } -@NgModule({ - declarations: - [ - ], - imports: [ - CommonModule, - SharedModule - ], - exports: [ - ] -}) -class CustomDialogModule { } diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html deleted file mode 100644 index d88900f1a5..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html +++ /dev/null @@ -1,60 +0,0 @@ - - -
-
-
-
- - legend.direction - - - {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} - - - - - legend.position - - - {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} - - - - - {{ 'legend.sort-legend' | translate }} - - - {{ 'legend.show-min' | translate }} - - - {{ 'legend.show-max' | translate }} - - - {{ 'legend.show-avg' | translate }} - - - {{ 'legend.show-total' | translate }} - -
-
-
-
- diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts deleted file mode 100644 index c977155428..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -/// -/// 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 { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; -import { OverlayRef } from '@angular/cdk/overlay'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { - LegendConfig, - LegendDirection, - legendDirectionTranslationMap, - LegendPosition, - legendPositionTranslationMap -} from '@shared/models/widget.models'; - -export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken('LegendConfigPanelData'); - -export interface LegendConfigPanelData { - legendConfig: LegendConfig; - legendConfigUpdated: (legendConfig: LegendConfig) => void; -} - -@Component({ - selector: 'tb-legend-config-panel', - templateUrl: './legend-config-panel.component.html', - styleUrls: ['./legend-config-panel.component.scss'] -}) -export class LegendConfigPanelComponent extends PageComponent implements OnInit { - - legendConfigForm: FormGroup; - - legendDirection = LegendDirection; - - legendDirections = Object.keys(LegendDirection); - - legendDirectionTranslations = legendDirectionTranslationMap; - - legendPosition = LegendPosition; - - legendPositions = Object.keys(LegendPosition); - - legendPositionTranslations = legendPositionTranslationMap; - - constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData, - public overlayRef: OverlayRef, - protected store: Store, - public fb: FormBuilder, - public viewContainerRef: ViewContainerRef) { - super(store); - } - - ngOnInit(): void { - this.legendConfigForm = this.fb.group({ - direction: [this.data.legendConfig.direction, []], - position: [this.data.legendConfig.position, []], - sortDataKeys: [this.data.legendConfig.sortDataKeys, []], - showMin: [this.data.legendConfig.showMin, []], - showMax: [this.data.legendConfig.showMax, []], - showAvg: [this.data.legendConfig.showAvg, []], - showTotal: [this.data.legendConfig.showTotal, []] - }); - this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => { - this.onDirectionChanged(direction); - }); - this.onDirectionChanged(this.data.legendConfig.direction); - this.legendConfigForm.valueChanges.subscribe(() => { - this.update(); - }); - } - - private onDirectionChanged(direction: LegendDirection) { - if (direction === LegendDirection.row) { - let position: LegendPosition = this.legendConfigForm.get('position').value; - if (position !== LegendPosition.bottom && position !== LegendPosition.top) { - position = LegendPosition.bottom; - } - this.legendConfigForm.patchValue( - { - position - }, {emitEvent: false} - ); - } - } - - update() { - const newLegendConfig: LegendConfig = this.legendConfigForm.value; - this.data.legendConfigUpdated(newLegendConfig); - } - -} diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html index fd02ac6b34..7d824a0186 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.html @@ -15,9 +15,43 @@ limitations under the License. --> - +
+
+ + legend.direction + + + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }} + + + + + legend.position + + + {{ legendPositionTranslations.get(legendPosition[pos]) | translate }} + + + +
+
+ + {{ 'legend.show-min' | translate }} + + + {{ 'legend.show-max' | translate }} + + + {{ 'legend.show-avg' | translate }} + + + {{ 'legend.show-total' | translate }} + + + {{ 'legend.sort-legend' | translate }} + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts index 6072bf7eaf..1fa5e166f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts @@ -14,32 +14,17 @@ /// limitations under the License. /// +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { isDefined } from '@core/utils'; import { - Component, - forwardRef, - Inject, - Injector, - Input, - OnDestroy, - OnInit, - StaticProvider, - ViewChild, - ViewContainerRef -} from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { DOCUMENT } from '@angular/common'; -import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; -import { ComponentPortal } from '@angular/cdk/portal'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; -import { WINDOW } from '@core/services/window.service'; -import { deepClone } from '@core/utils'; -import { LegendConfig } from '@shared/models/widget.models'; -import { - LEGEND_CONFIG_PANEL_DATA, - LegendConfigPanelComponent, - LegendConfigPanelData -} from '@home/components/widget/legend-config-panel.component'; + LegendConfig, + LegendDirection, + legendDirectionTranslationMap, + LegendPosition, + legendPositionTranslationMap +} from '@shared/models/widget.models'; +import { Subscription } from 'rxjs'; // @dynamic @Component({ @@ -58,105 +43,60 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc @Input() disabled: boolean; - @ViewChild('legendConfigPanelOrigin') legendConfigPanelOrigin: CdkOverlayOrigin; - - innerValue: LegendConfig; + legendConfigForm: FormGroup; + legendDirection = LegendDirection; + legendDirections = Object.keys(LegendDirection); + legendDirectionTranslations = legendDirectionTranslationMap; + legendPosition = LegendPosition; + legendPositions = Object.keys(LegendPosition); + legendPositionTranslations = legendPositionTranslationMap; + private legendSettingsFormChanges$: Subscription; + private legendSettingsFormDirectionChanges$: Subscription; private propagateChange = (_: any) => {}; - constructor(private overlay: Overlay, - public viewContainerRef: ViewContainerRef, - public breakpointObserver: BreakpointObserver, - @Inject(DOCUMENT) private document: Document, - @Inject(WINDOW) private window: Window) { + constructor(private fb: FormBuilder) { } ngOnInit(): void { + this.legendConfigForm = this.fb.group({ + direction: [null, []], + position: [null, []], + sortDataKeys: [null, []], + showMin: [null, []], + showMax: [null, []], + showAvg: [null, []], + showTotal: [null, []] + }); + this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges + .subscribe((direction: LegendDirection) => { + this.onDirectionChanged(direction); + }); + this.legendSettingsFormChanges$ = this.legendConfigForm.valueChanges.subscribe( + () => this.legendConfigUpdated() + ); } - ngOnDestroy(): void { - } - - openEditMode() { - if (this.disabled) { - return; - } - const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); - const position = this.overlay.position(); - const config = new OverlayConfig({ - panelClass: 'tb-legend-config-panel', - backdropClass: 'cdk-overlay-transparent-backdrop', - hasBackdrop: isGtSm, - }); - if (isGtSm) { - config.minWidth = '220px'; - config.maxHeight = '300px'; - const panelHeight = 220; - const panelWidth = 220; - const el = this.legendConfigPanelOrigin.elementRef.nativeElement; - const offset = el.getBoundingClientRect(); - const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; - const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; - const bottomY = offset.bottom - scrollTop; - const leftX = offset.left - scrollLeft; - let originX; - let originY; - let overlayX; - let overlayY; - const wHeight = this.document.documentElement.clientHeight; - const wWidth = this.document.documentElement.clientWidth; - if (bottomY + panelHeight > wHeight) { - originY = 'top'; - overlayY = 'bottom'; - } else { - originY = 'bottom'; - overlayY = 'top'; + private onDirectionChanged(direction: LegendDirection) { + if (direction === LegendDirection.row) { + let position: LegendPosition = this.legendConfigForm.get('position').value; + if (position !== LegendPosition.bottom && position !== LegendPosition.top) { + position = LegendPosition.bottom; } - if (leftX + panelWidth > wWidth) { - originX = 'end'; - overlayX = 'end'; - } else { - originX = 'start'; - overlayX = 'start'; - } - const connectedPosition: ConnectedPosition = { - originX, - originY, - overlayX, - overlayY - }; - config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef) - .withPositions([connectedPosition]); - } else { - config.minWidth = '100%'; - config.minHeight = '100%'; - config.positionStrategy = position.global().top('0%').left('0%') - .right('0%').bottom('0%'); + this.legendConfigForm.patchValue({position}, {emitEvent: false} + ); } - - const overlayRef = this.overlay.create(config); - - overlayRef.backdropClick().subscribe(() => { - overlayRef.dispose(); - }); - - const injector = this._createLegendConfigPanelInjector( - overlayRef, - { - legendConfig: deepClone(this.innerValue), - legendConfigUpdated: this.legendConfigUpdated.bind(this) - } - ); - - overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); } - private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector { - const providers: StaticProvider[] = [ - {provide: LEGEND_CONFIG_PANEL_DATA, useValue: data}, - {provide: OverlayRef, useValue: overlayRef} - ]; - return Injector.create({parent: this.viewContainerRef.injector, providers}); + ngOnDestroy(): void { + if (this.legendSettingsFormDirectionChanges$) { + this.legendSettingsFormDirectionChanges$.unsubscribe(); + this.legendSettingsFormDirectionChanges$ = null; + } + if (this.legendSettingsFormChanges$) { + this.legendSettingsFormChanges$.unsubscribe(); + this.legendSettingsFormChanges$ = null; + } } registerOnChange(fn: any): void { @@ -168,14 +108,29 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.legendConfigForm.disable({emitEvent: false}); + } else { + this.legendConfigForm.enable({emitEvent: false}); + } } - writeValue(obj: LegendConfig): void { - this.innerValue = obj; + writeValue(legendConfig: LegendConfig): void { + if (legendConfig) { + this.legendConfigForm.patchValue({ + direction: legendConfig.direction, + position: legendConfig.position, + sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false, + showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false, + showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false, + showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false, + showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false + }, {emitEvent: false}); + } + this.onDirectionChanged(legendConfig.direction); } - private legendConfigUpdated(legendConfig: LegendConfig) { - this.innerValue = legendConfig; - this.propagateChange(this.innerValue); + private legendConfigUpdated() { + this.propagateChange(this.legendConfigForm.value); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 7c88c79e2b..a86da23f59 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -136,7 +136,7 @@ alarm.no-alarms-prompt + class="no-data-found">{{ noDataDisplayMessageText }} {{ 'common.loading' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 74e047b0a7..7b32d84e71 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -48,7 +48,7 @@ import cssjs from '@core/css/css'; import { sortItems } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; -import { BehaviorSubject, forkJoin, fromEvent, merge, Observable } from 'rxjs'; +import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { entityTypeTranslations } from '@shared/models/entity-type.models'; import { debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators'; @@ -74,6 +74,7 @@ import { getColumnWidth, getRowStyleInfo, getTableCellButtonActions, + noDataMessage, prepareTableCellButtonActions, RowStyleInfo, TableCellButtonActionDescriptor, @@ -166,6 +167,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public columns: Array = []; public displayedColumns: string[] = []; public alarmsDatasource: AlarmsDatasource; + public noDataDisplayMessageText: string; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -193,6 +195,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private rowStylesInfo: RowStyleInfo; + private widgetTimewindowChanged$: Subscription; + private searchAction: WidgetAction = { name: 'action.search', show: true, @@ -248,6 +252,19 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.initializeConfig(); this.updateAlarmSource(); this.ctx.updateWidgetParams(); + + if (this.displayPagination) { + this.widgetTimewindowChanged$ = this.ctx.defaultSubscription.widgetTimewindowChanged$.subscribe( + () => this.pageLink.page = 0 + ); + } + } + + ngOnDestroy(): void { + if (this.widgetTimewindowChanged$) { + this.widgetTimewindowChanged$.unsubscribe(); + this.widgetTimewindowChanged$ = null; + } } ngAfterViewInit(): void { @@ -342,6 +359,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.pageLink.severityList = isDefined(this.widgetConfig.alarmSeverityList) ? this.widgetConfig.alarmSeverityList : []; this.pageLink.typeList = isDefined(this.widgetConfig.alarmTypeList) ? this.widgetConfig.alarmTypeList : []; + this.noDataDisplayMessageText = + noDataMessage(this.widgetConfig.noDataDisplayMessage, 'alarm.no-alarms-prompt', this.utils, this.translate); + const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); cssParser.testMode = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts index 898ceb3b81..8e99d4f428 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/canvas-digital-gauge.ts @@ -276,7 +276,6 @@ export class CanvasDigitalGauge extends BaseGauge { } } } - options.ticksValue = []; for (const tick of options.ticks) { if (tick !== null) { @@ -401,10 +400,8 @@ export class CanvasDigitalGauge extends BaseGauge { const context = this.contextValueClone; // clear the cache context.clearRect(x, y, w, h); - context.save(); context.drawImage(canvas.elementClone, x, y, w, h); - context.save(); drawDigitalValue(context, options, this.elementValueClone.renderedValue); @@ -427,10 +424,8 @@ export class CanvasDigitalGauge extends BaseGauge { const context = this.contextProgressClone; // clear the cache context.clearRect(x, y, w, h); - context.save(); context.drawImage(this.elementValueClone, x, y, w, h); - context.save(); if (Number(fixedProgress) > 0) { drawProgress(context, options, progress); @@ -444,10 +439,8 @@ export class CanvasDigitalGauge extends BaseGauge { // clear the canvas canvas.context.clearRect(x, y, w, h); - canvas.context.save(); canvas.context.drawImage(this.elementProgressClone, x, y, w, h); - canvas.context.save(); // @ts-ignore super.draw(); @@ -755,6 +748,7 @@ function drawDigitalTitle(context: DigitalGaugeCanvasRenderingContext2D, options context.font = Drawings.font(options, 'Title', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'Title', options.title.toUpperCase(), textX, textY); + context.restore(); } function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { @@ -773,6 +767,7 @@ function drawDigitalLabel(context: DigitalGaugeCanvasRenderingContext2D, options context.font = Drawings.font(options, 'Label', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'Label', options.label, textX, textY); + context.restore(); } function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions) { @@ -790,6 +785,7 @@ function drawDigitalMinMax(context: DigitalGaugeCanvasRenderingContext2D, option context.lineWidth = 0; drawText(context, options, 'MinMax', options.minValue + '', minX, minY); drawText(context, options, 'MinMax', options.maxValue + '', maxX, maxY); + context.restore(); } function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, value: any) { @@ -812,6 +808,7 @@ function drawDigitalValue(context: DigitalGaugeCanvasRenderingContext2D, options context.font = Drawings.font(options, 'Value', fontSizeFactor); context.lineWidth = 0; drawText(context, options, 'Value', text, textX, textY); + context.restore(); } function getProgressColor(progress: number, colorsRange: DigitalGaugeColorRange[]): string { @@ -820,7 +817,7 @@ function getProgressColor(progress: number, colorsRange: DigitalGaugeColorRange[ return colorsRange[0].rgbString; } - for (let j = 0; j < colorsRange.length; j++) { + for (let j = 1; j < colorsRange.length; j++) { if (progress <= colorsRange[j].pct) { const lower = colorsRange[j - 1]; const upper = colorsRange[j]; @@ -836,6 +833,7 @@ function getProgressColor(progress: number, colorsRange: DigitalGaugeColorRange[ return color.toRgbString(); } } + return colorsRange[colorsRange.length - 1].rgbString; } function drawArcGlow(context: DigitalGaugeCanvasRenderingContext2D, @@ -944,6 +942,7 @@ function drawTickBar(context: DigitalGaugeCanvasRenderingContext2D, tickValues: function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, options: CanvasDigitalGaugeOptions, progress: number) { let neonColor; + context.save(); if (options.neonGlowBrightness) { context.currentColor = neonColor = getProgressColor(progress, options.neonColorsRange); } else { @@ -1019,4 +1018,5 @@ function drawProgress(context: DigitalGaugeCanvasRenderingContext2D, true, options.colorTicks, options.tickWidth); } + context.restore(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts index 0897d7a8eb..c115f23dbe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.models.ts @@ -246,7 +246,8 @@ export const digitalGaugeSettingsSchema: JsonSettingsSchema = { title: 'Color', type: 'string' } - } + }, + required: ['color'], } }, showTicks: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts index a2aadbb9b3..3385739854 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts @@ -266,7 +266,6 @@ export class TbCanvasDigitalGauge { init() { let updateSetting = false; - if (this.localSettings.useFixedLevelColor && this.localSettings.fixedLevelColors?.length > 0) { this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors); updateSetting = true; @@ -285,6 +284,11 @@ export class TbCanvasDigitalGauge { let levelColorsDatasource: Datasource[] = []; const predefineLevelColors: ColorLevelSetting[] = []; + predefineLevelColors.push({ + value: this.localSettings.minValue, + color: this.localSettings.gaugeColor + }); + function setLevelColor(levelSetting: AttributeSourceProperty, color: string) { if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) { predefineLevelColors.push({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index ddf4972dc7..74ccd53ec2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -95,7 +95,7 @@ entity.no-entities-prompt + class="no-data-found">{{ noDataDisplayMessageText }} {{ 'common.loading' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index f1617eb288..bd52f2ab9b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -80,6 +80,7 @@ import { getEntityValue, getRowStyleInfo, getTableCellButtonActions, + noDataMessage, prepareTableCellButtonActions, RowStyleInfo, TableCellButtonActionDescriptor, @@ -142,6 +143,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public columns: Array = []; public displayedColumns: string[] = []; public entityDatasource: EntityDatasource; + public noDataDisplayMessageText: string; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -275,6 +277,9 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; + this.noDataDisplayMessageText = + noDataMessage(this.widgetConfig.noDataDisplayMessage, 'entity.no-entities-prompt', this.utils, this.translate); + const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); cssParser.testMode = false; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts index 67acd1e04b..d1645e3f4c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts @@ -19,7 +19,7 @@ import { DataKey, Datasource, DatasourceData, JsonSettingsSchema } from '@shared/models/widget.models'; import * as moment_ from 'moment'; -import { DataKeyType } from "@shared/models/telemetry/telemetry.models"; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { ComparisonDuration } from '@shared/models/time/time.models'; export declare type ChartType = 'line' | 'pie' | 'bar' | 'state' | 'graph'; @@ -466,7 +466,7 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { schema.form.push({ key: 'tooltipValueFormatter', type: 'javascript', - helpId: 'widget/lib/tooltip_value_format_fn' + helpId: 'widget/lib/flot/tooltip_value_format_fn' }); schema.form.push('hideZeros'); schema.form.push('showTooltip'); @@ -516,7 +516,8 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { }, { key: 'yaxis.ticksFormatter', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/flot/ticks_formatter_fn' } ] }); @@ -531,11 +532,11 @@ export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema { schema.form.push(chartSettingsSchemaForComparison.form, chartSettingsSchemaForCustomLegend.form); schema.groupInfoes.push({ formIndex: schema.groupInfoes.length, - GroupTitle:'Comparison Settings' + GroupTitle: 'Comparison Settings' }); schema.groupInfoes.push({ formIndex: schema.groupInfoes.length, - GroupTitle:'Custom Legend Settings' + GroupTitle: 'Custom Legend Settings' }); } return schema; @@ -981,13 +982,15 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean, chartType: }, { key: 'pointShapeFormatter', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/flot/point_shape_format_fn' }, 'showPointsLineWidth', 'showPointsRadius', { key: 'tooltipValueFormatter', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/flot/tooltip_value_format_fn' }, 'showSeparateAxis', 'axisMin', @@ -1012,7 +1015,8 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean, chartType: }, { key: 'axisTicksFormatter', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/flot/ticks_formatter_fn' } ] }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html index 81c06fe0f7..3c9cc99eca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html @@ -22,7 +22,7 @@ class="tb-json-input__form" [formGroup]="attributeUpdateFormGroup" (ngSubmit)="save()"> -
+
-
-
- {{ 'widgets.input-widgets.no-entity-selected' | translate }} -
-
- {{ 'widgets.input-widgets.no-datakey-selected' | translate }} -
-
+ +
{{ errorMessage | translate }}
-
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss index f097de5afb..217c91dc4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss @@ -25,7 +25,6 @@ } &__error { - text-align: center; font-size: 18px; color: #a0a0a0; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts index ce174fcde0..9fcf4c13bd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts @@ -28,7 +28,7 @@ import { AttributeService } from '@core/http/attribute.service'; import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityType } from '@shared/models/entity-type.models'; -import { createLabelFromDatasource } from '@core/utils'; +import { createLabelFromDatasource, isDefinedAndNotNull } from '@core/utils'; import { Observable } from 'rxjs'; enum JsonInputWidgetMode { @@ -63,9 +63,7 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit { labelValue: string; - entityDetected = false; - dataKeyDetected = false; - isValidParameter = false; + datasourceDetected = false; errorMessage: string; isFocused: boolean; @@ -111,30 +109,26 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit { } private validateDatasources() { - if (this.datasource?.type === DatasourceType.entity) { - this.entityDetected = true; - if (this.datasource.dataKeys.length) { - this.dataKeyDetected = true; - - if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { - if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { - if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) { - this.isValidParameter = true; - } else { - this.errorMessage = 'widgets.input-widgets.not-allowed-entity'; - } - } else { - this.errorMessage = 'widgets.input-widgets.no-attribute-selected'; + this.datasourceDetected = isDefinedAndNotNull(this.datasource); + if (!this.datasourceDetected) { + return; + } + if (this.datasource.type === DatasourceType.entity) { + if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) { + if (this.datasource.dataKeys[0].type === DataKeyType.attribute) { + if (this.settings.attributeScope !== AttributeScope.SERVER_SCOPE && this.datasource.entityType !== EntityType.DEVICE) { + this.errorMessage = 'widgets.input-widgets.not-allowed-entity'; } } else { - if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) { - this.isValidParameter = true; - } else { - this.errorMessage = 'widgets.input-widgets.no-timeseries-selected'; - } + this.errorMessage = 'widgets.input-widgets.no-attribute-selected'; + } + } else { + if (this.datasource.dataKeys[0].type !== DataKeyType.timeseries) { + this.errorMessage = 'widgets.input-widgets.no-timeseries-selected'; } - } + } else { + this.errorMessage = 'widgets.input-widgets.no-entity-selected'; } } @@ -152,7 +146,7 @@ export class JsonInputWidgetComponent extends PageComponent implements OnInit { } private updateWidgetData(data: Array) { - if (this.isValidParameter) { + if (!this.errorMessage) { let value = {}; if (data[0].data[0][1] !== '') { try { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts index a0950eb53f..9f1810f82f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts @@ -16,7 +16,7 @@ import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models'; import { - createLabelFromDatasource, deepClone, + createLabelFromDatasource, hashCode, isDefined, isDefinedAndNotNull, @@ -127,7 +127,6 @@ export function aspectCache(imageUrl: string): Observable { export type TranslateFunc = (key: string, defaultTranslation?: string) => string; -const varsRegex = /\${([^}]*)}/g; const linkActionRegex = /([^<]*)<\/link-act>/g; const buttonActionRegex = /([^<]*)<\/button-act>/g; @@ -304,7 +303,8 @@ export const parseWithTranslation = { if (this.translateFn) { return this.translateFn(key, defaultTranslation); } else { - throw console.error('Translate not assigned'); + console.error('Translate not assigned'); + throw Error('Translate not assigned'); } }, parseTemplate(template: string, data: object, forceTranslate = false): string { @@ -330,12 +330,12 @@ export function parseData(input: DatasourceData[], dataIndex?: number): Formatte deviceType: null }; entityArray.filter(el => el.data.length).forEach(el => { - dataIndex = isDefined(dataIndex) ? dataIndex : el.data.length - 1; - if (!obj.hasOwnProperty(el.dataKey.label) || el.data[dataIndex][1] !== '') { - obj[el.dataKey.label] = el.data[dataIndex][1]; - obj[el.dataKey.label + '|ts'] = el.data[dataIndex][0]; - if (el.dataKey.label === 'type') { - obj.deviceType = el.data[dataIndex][1]; + const index = isDefined(dataIndex) ? dataIndex : el.data.length - 1; + if (!obj.hasOwnProperty(el.dataKey.label) || el.data[index][1] !== '') { + obj[el.dataKey.label] = el.data[index][1]; + obj[el.dataKey.label + '|ts'] = el.data[index][0]; + if (el.dataKey.label.toLowerCase() === 'type') { + obj.deviceType = el.data[index][1]; } } }); @@ -360,28 +360,35 @@ export function flatData(input: FormattedData[]): FormattedData { } export function parseArray(input: DatasourceData[]): FormattedData[][] { - return _(input).groupBy(el => el?.datasource?.entityName) - .values().value().map((entityArray) => - entityArray[0].data.map((el, i) => { - const obj: FormattedData = { - entityName: entityArray[0]?.datasource?.entityName, - entityId: entityArray[0]?.datasource?.entityId, - entityType: entityArray[0]?.datasource?.entityType, - $datasource: entityArray[0]?.datasource, - dsIndex: i, - time: el[0], - deviceType: null - }; - entityArray.filter(e => e.data.length && e.data[i]).forEach(entity => { - obj[entity?.dataKey?.label] = entity?.data[i][1]; - obj[entity?.dataKey?.label + '|ts'] = entity?.data[0][0]; - if (entity?.dataKey?.label === 'type') { - obj.deviceType = entity?.data[0][1]; + return _(input).groupBy(el => el.datasource.entityName) + .values().value().map((entityArray, dsIndex) => { + const timeDataMap: {[time: number]: FormattedData} = {}; + entityArray.filter(e => e.data.length).forEach(entity => { + entity.data.forEach(tsData => { + const time = tsData[0]; + const value = tsData[1]; + let data = timeDataMap[time]; + if (!data) { + data = { + entityName: entity.datasource.entityName, + entityId: entity.datasource.entityId, + entityType: entity.datasource.entityType, + $datasource: entity.datasource, + dsIndex, + time, + deviceType: null + }; + timeDataMap[time] = data; + } + data[entity.dataKey.label] = value; + data[entity.dataKey.label + '|ts'] = time; + if (entity.dataKey.label.toLowerCase() === 'type') { + data.deviceType = value; } }); - return obj; - }) - ); + }); + return _.values(timeDataMap); + }); } export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html new file mode 100644 index 0000000000..93fa72c84b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.html @@ -0,0 +1,54 @@ + +
+ +

widgets.maps.select-entity

+ + +
+ + +
+
+ + entity.entity + + + {{ entity.entityName }} + + + +
widgets.maps.select-entity-hint
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.scss similarity index 77% rename from ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.scss index 1432b28318..b9600d8731 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.scss @@ -14,19 +14,8 @@ * limitations under the License. */ :host { - width: 100%; - height: 100%; - form, - fieldset { - height: 100%; - } - - .mat-content { - overflow: hidden; - background-color: #fff; - } - - .mat-padding { - padding: 16px; + .tb-hint { + margin-top: -1.25em; + max-width: fit-content; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts new file mode 100644 index 0000000000..58526a4e66 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts @@ -0,0 +1,56 @@ +/// +/// 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 { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { FormattedData } from '@home/components/widget/lib/maps/map-models'; + +export interface SelectEntityDialogData { + entities: FormattedData[]; +} + +@Component({ + selector: 'tb-select-entity-dialog', + templateUrl: './select-entity-dialog.component.html', + styleUrls: ['./select-entity-dialog.component.scss'] +}) +export class SelectEntityDialogComponent extends DialogComponent { + + selectEntityFormGroup: FormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: SelectEntityDialogData, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store, router, dialogRef); + + this.selectEntityFormGroup = this.fb.group( + { + entity: ['', Validators.required] + } + ); + } + + save(): void { + this.dialogRef.close(this.selectEntityFormGroup.value.entity); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index fb79b44e35..27d6fb5edc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -14,23 +14,15 @@ /// limitations under the License. /// -import L, { - FeatureGroup, - Icon, - LatLngBounds, - LatLngTuple, - markerClusterGroup, - MarkerClusterGroup, - MarkerClusterGroupOptions, Projection -} from 'leaflet'; +import L, { FeatureGroup, Icon, LatLngBounds, LatLngTuple, Projection } from 'leaflet'; import tinycolor from 'tinycolor2'; import 'leaflet-providers'; -import 'leaflet.markercluster/dist/leaflet.markercluster'; +import { MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet.markercluster/dist/leaflet.markercluster'; +import '@geoman-io/leaflet-geoman-free'; import { defaultSettings, FormattedData, - MapProviders, MapSettings, MarkerSettings, PolygonSettings, @@ -42,9 +34,7 @@ import { Marker } from './markers'; import { Observable, of } from 'rxjs'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; -import { - createTooltip, -} from '@home/components/widget/lib/maps/maps-utils'; +import { createTooltip } from '@home/components/widget/lib/maps/maps-utils'; import { checkLngLat, createLoadingDiv, @@ -54,6 +44,12 @@ import { } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { deepClone, isDefinedAndNotNull, isEmptyStr, isString } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; +import { + SelectEntityDialogComponent, + SelectEntityDialogData +} from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; export default abstract class LeafletMap { @@ -78,20 +74,24 @@ export default abstract class LeafletMap { drawRoutes: boolean; showPolygon: boolean; updatePending = false; + editPolygons = false; + selectedEntity: FormattedData; addMarkers: L.Marker[] = []; addPolygons: L.Polygon[] = []; // tslint:disable-next-line:no-string-literal southWest = new L.LatLng(-Projection.SphericalMercator['MAX_LATITUDE'], -180); // tslint:disable-next-line:no-string-literal northEast = new L.LatLng(Projection.SphericalMercator['MAX_LATITUDE'], 180); - mousePositionOnMap: L.LatLng; - addMarker: L.Control; - addPolygon: L.Control; + saveLocation: (e: FormattedData, values: {[key: string]: any}) => Observable; + saveMarkerLocation: (e: FormattedData, lat?: number, lng?: number) => Observable; + savePolygonLocation: (e: FormattedData, coordinates?: Array) => Observable; protected constructor(public ctx: WidgetContext, public $container: HTMLElement, options: UnitedMapSettings) { this.options = options; + this.editPolygons = options.showPolygon && options.editablePolygon; + L.Icon.Default.imagePath = '/'; } public initSettings(options: MapSettings) { @@ -102,16 +102,28 @@ export default abstract class LeafletMap { removeOutsideVisibleBounds, animate, chunkedLoading, + spiderfyOnMaxZoom, maxClusterRadius, maxZoom }: MapSettings = options; if (useClusterMarkers) { + // disabled marker cluster icon + (L as any).MarkerCluster = (L as any).MarkerCluster.extend({ + options: { pmIgnore: true, ...L.Icon.prototype.options } + }); const clusteringSettings: MarkerClusterGroupOptions = { - spiderfyOnMaxZoom: false, + spiderfyOnMaxZoom, zoomToBoundsOnClick: zoomOnClick, showCoverageOnHover, removeOutsideVisibleBounds, animate, - chunkedLoading + chunkedLoading, + pmIgnore: true, + spiderLegPolylineOptions: { + pmIgnore: true + }, + polygonOptions: { + pmIgnore: true + } }; if (maxClusterRadius && maxClusterRadius > 0) { clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius); @@ -119,183 +131,166 @@ export default abstract class LeafletMap { if (maxZoom && maxZoom >= 0 && maxZoom < 19) { clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom); } - this.markersCluster = markerClusterGroup(clusteringSettings); + this.markersCluster = new MarkerClusterGroup(clusteringSettings); } } - addMarkerControl() { - if (this.options.draggableMarker) { - this.map.on('mousemove', (e: L.LeafletMouseEvent) => { - this.mousePositionOnMap = e.latlng; - }); - const dragListener = (e: L.DragEndEvent) => { - if (e.type === 'dragend' && this.mousePositionOnMap) { - const icon = L.icon({ - iconRetinaUrl: 'marker-icon-2x.png', - iconUrl: 'marker-icon.png', - shadowUrl: 'marker-shadow.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - popupAnchor: [1, -34], - tooltipAnchor: [16, -28], - shadowSize: [41, 41] - }); - const customLatLng = this.convertToCustomFormat(this.mousePositionOnMap); - this.mousePositionOnMap.lat = customLatLng[this.options.latKeyName]; - this.mousePositionOnMap.lng = customLatLng[this.options.lngKeyName]; - - const newMarker = L.marker(this.mousePositionOnMap, { icon }).addTo(this.map); - this.addMarkers.push(newMarker); - const datasourcesList = document.createElement('div'); - const header = document.createElement('p'); - header.appendChild(document.createTextNode('Select entity:')); - header.setAttribute('style', 'font-size: 14px; margin: 8px 0'); - datasourcesList.appendChild(header); - this.datasources.forEach(ds => { - const dsItem = document.createElement('p'); - dsItem.appendChild(document.createTextNode(ds.entityName)); - dsItem.setAttribute('style', 'font-size: 14px; margin: 8px 0; cursor: pointer'); - dsItem.onclick = () => { - const updatedEnttity = { ...ds, ...customLatLng }; - this.saveMarkerLocation(updatedEnttity).subscribe(() => { - this.map.removeLayer(newMarker); - const markerIndex = this.addMarkers.indexOf(newMarker); - if (markerIndex > -1) { - this.addMarkers.splice(markerIndex, 1); - } - this.deleteMarker(ds.entityName); - this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options); - }); - }; - datasourcesList.appendChild(dsItem); - }); - datasourcesList.appendChild(document.createElement('br')); - const deleteBtn = document.createElement('a'); - deleteBtn.appendChild(document.createTextNode('Discard changes')); - deleteBtn.onclick = () => { - this.map.removeLayer(newMarker); - const markerIndex = this.addMarkers.indexOf(newMarker); - if (markerIndex > -1) { - this.addMarkers.splice(markerIndex, 1); - } - }; - datasourcesList.appendChild(deleteBtn); - const popup = L.popup(); - popup.setContent(datasourcesList); - newMarker.bindPopup(popup).openPopup(); - } - this.addMarker.setPosition('topright'); - }; - const addMarker = L.Control.extend({ - onAdd() { - const img = L.DomUtil.create('img') as any; - img.src = `assets/add_location.svg`; - img.style.width = '32px'; - img.style.height = '32px'; - img.title = 'Drag and drop to add marker'; - img.onclick = this.dragMarker; - img.draggable = true; - const draggableImg = new L.Draggable(img); - draggableImg.enable(); - draggableImg.on('dragend', dragListener); - return img; - }, - onRemove() { - }, - dragMarker: this.dragMarker - } as any); - const addMarkerControl = (opts) => { - return new addMarker(opts); - }; - this.addMarker = addMarkerControl({ position: 'topright' }).addTo(this.map); + private selectEntityWithoutLocationDialog(shapes: L.PM.SUPPORTED_SHAPES): Observable { + let entities; + switch (shapes) { + case 'Polygon': + case 'Rectangle': + entities = this.datasources.filter(pData => !this.isValidPolygonPosition(pData)); + break; + case 'Marker': + entities = this.datasources.filter(mData => !this.convertPosition(mData)); + break; + default: + return of(null); + } + if (entities.length === 1) { + return of(entities[0]); + } + const dialog = this.ctx.$injector.get(MatDialog); + return dialog.open(SelectEntityDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entities + } + }).afterClosed(); + } + + private selectEntityWithoutLocation(type: string) { + this.selectEntityWithoutLocationDialog(type.substring(2)).subscribe((data) => { + if (data !== null) { + this.selectedEntity = data; + this.toggleDrawMode(type); + } else { + // @ts-ignore + this.map.pm.Toolbar.toggleButton(type, false); } + }); + } + + private toggleDrawMode(type: string) { + this.map.pm.Draw[type].toggle(); } - addPolygonControl() { - if (this.options.showPolygon && this.options.editablePolygon) { - this.map.on('mousemove', (e: L.LeafletMouseEvent) => { - this.mousePositionOnMap = e.latlng; + addEditControl() { + // Customize edit marker + if (this.options.draggableMarker) { + const actions = [{ + text: L.PM.Utils.getTranslation('actions.cancel'), + onClick: () => this.toggleDrawMode('tbMarker') + }]; + + this.map.pm.Toolbar.copyDrawControl('Marker', { + name: 'tbMarker', + afterClick: () => this.selectEntityWithoutLocation('tbMarker'), + disabled: true, + actions + }); + } + + // Customize edit polygon + if (this.editPolygons) { + const rectangleActions = [ + { + text: L.PM.Utils.getTranslation('actions.cancel'), + onClick: () => this.toggleDrawMode('tbRectangle') + } + ]; + + const polygonActions = [ + 'finish' as const, + 'removeLastVertex' as const, + { + text: L.PM.Utils.getTranslation('actions.cancel'), + onClick: () => this.toggleDrawMode('tbPolygon') + } + ]; + + this.map.pm.Toolbar.copyDrawControl('Rectangle', { + name: 'tbRectangle', + afterClick: () => this.selectEntityWithoutLocation('tbRectangle'), + disabled: true, + actions: rectangleActions + }); + + this.map.pm.Toolbar.copyDrawControl('Polygon', { + name: 'tbPolygon', + afterClick: () => this.selectEntityWithoutLocation('tbPolygon'), + disabled: true, + actions: polygonActions + }); + } + + const translateService = this.ctx.$injector.get(TranslateService); + this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en'); + this.map.pm.addControls({ + position: 'topleft', + drawMarker: false, + drawCircle: false, + drawCircleMarker: false, + drawRectangle: false, + drawPolyline: false, + drawPolygon: false, + editMode: this.editPolygons, + cutPolygon: this.editPolygons, + rotateMode: this.editPolygons }); - const dragListener = (e: L.DragEndEvent) => { - if (e.type === 'dragend') { - const polygonOffset = this.options.provider === MapProviders.image ? 10 : 0.01; - - const convert = this.convertToCustomFormat(this.mousePositionOnMap, polygonOffset); - this.mousePositionOnMap.lat = convert[this.options.latKeyName]; - this.mousePositionOnMap.lng = convert[this.options.lngKeyName]; - - const latlng1 = this.mousePositionOnMap; - const latlng2 = L.latLng(this.mousePositionOnMap.lat, this.mousePositionOnMap.lng + polygonOffset); - const latlng3 = L.latLng(this.mousePositionOnMap.lat - polygonOffset, this.mousePositionOnMap.lng); - const polygonPoints = [latlng1, latlng2, latlng3]; - - const newPolygon = L.polygon(polygonPoints).addTo(this.map); - this.addPolygons.push(newPolygon); - const datasourcesList = document.createElement('div'); - const customLatLng = {[this.options.polygonKeyName]: this.convertToPolygonFormat(polygonPoints)}; - const header = document.createElement('p'); - header.appendChild(document.createTextNode('Select entity:')); - header.setAttribute('style', 'font-size: 14px; margin: 8px 0'); - datasourcesList.appendChild(header); - this.datasources.forEach(ds => { - const dsItem = document.createElement('p'); - dsItem.appendChild(document.createTextNode(ds.entityName)); - dsItem.setAttribute('style', 'font-size: 14px; margin: 8px 0; cursor: pointer'); - dsItem.onclick = () => { - const updatedEnttity = { ...ds, ...customLatLng }; - this.savePolygonLocation(updatedEnttity).subscribe(() => { - this.map.removeLayer(newPolygon); - const polygonIndex = this.addPolygons.indexOf(newPolygon); - if (polygonIndex > -1) { - this.addPolygons.splice(polygonIndex, 1); - } - this.deletePolygon(ds.entityName); - }); - }; - datasourcesList.appendChild(dsItem); + this.map.on('pm:create', (e) => { + if (e.shape === 'tbMarker') { + // @ts-ignore + this.saveLocation(this.selectedEntity, this.convertToCustomFormat(e.layer.getLatLng())).subscribe(() => { + }); + } else if (e.shape === 'tbRectangle' || e.shape === 'tbPolygon') { + // @ts-ignore + this.saveLocation(this.selectedEntity, this.convertPolygonToCustomFormat(e.layer.getLatLngs()[0])).subscribe(() => { }); - datasourcesList.appendChild(document.createElement('br')); - const deleteBtn = document.createElement('a'); - deleteBtn.appendChild(document.createTextNode('Discard changes')); - deleteBtn.onclick = () => { - this.map.removeLayer(newPolygon); - const polygonIndex = this.addPolygons.indexOf(newPolygon); - if (polygonIndex > -1) { - this.addPolygons.splice(polygonIndex, 1); - } - }; - datasourcesList.appendChild(deleteBtn); - const popup = L.popup(); - popup.setContent(datasourcesList); - newPolygon.bindPopup(popup).openPopup(); } - this.addPolygon.setPosition('topright'); - }; - const addPolygon = L.Control.extend({ - onAdd() { - const img = L.DomUtil.create('img') as any; - img.src = `assets/add_polygon.svg`; - img.style.width = '32px'; - img.style.height = '32px'; - img.title = 'Drag and drop to add Polygon'; - img.onclick = this.dragPolygonVertex; - img.draggable = true; - const draggableImg = new L.Draggable(img); - draggableImg.enable(); - draggableImg.on('dragend', dragListener); - return img; - }, - onRemove() { - }, - dragPolygonVertex: this.dragPolygonVertex - } as any); - const addPolygonControl = (opts) => { - return new addPolygon(opts); - }; - this.addPolygon = addPolygonControl({ position: 'topright' }).addTo(this.map); + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + }); + + this.map.on('pm:cut', (e) => { + // @ts-ignore + e.originalLayer.setLatLngs(e.layer.getLatLngs()); + e.originalLayer.addTo(this.map); + // @ts-ignore + e.originalLayer._pmTempLayer = false; + const iterator = this.polygons.values(); + let result = iterator.next(); + while (!result.done && e.originalLayer !== result.value.leafletPoly) { + result = iterator.next(); + } + // @ts-ignore + e.layer._pmTempLayer = true; + e.layer.remove(); + }); + + this.map.on('pm:remove', (e) => { + if (e.shape === 'Marker') { + const iterator = this.markers.values(); + let result = iterator.next(); + while (!result.done && e.layer !== result.value.leafletMarker) { + result = iterator.next(); + } + this.saveLocation(result.value.data, this.convertToCustomFormat(null)).subscribe(() => {}); + } else if (e.shape === 'Polygon') { + const iterator = this.polygons.values(); + let result = iterator.next(); + while (!result.done && e.layer !== result.value.leafletPoly) { + result = iterator.next(); + } + this.saveLocation(result.value.data, this.convertPolygonToCustomFormat(null)).subscribe(() => {}); + } + }); } - } public setLoading(loading: boolean) { if (this.loading !== loading) { @@ -337,11 +332,10 @@ export default abstract class LeafletMap { if (this.options.disableScrollZooming) { this.map.scrollWheelZoom.disable(); } - if (this.options.draggableMarker) { - this.addMarkerControl(); - } - if (this.options.editablePolygon) { - this.addPolygonControl(); + if (this.options.draggableMarker || this.editPolygons) { + this.addEditControl(); + } else { + this.map.pm.disableDraw(); } if (this.options.useClusterMarkers) { this.map.addLayer(this.markersCluster); @@ -352,14 +346,6 @@ export default abstract class LeafletMap { } } - public saveMarkerLocation(datasource: FormattedData, lat?: number, lng?: number): Observable { - return of(null); - } - - public savePolygonLocation(datasource: FormattedData, coordinates?: Array<[number, number]>): Observable { - return of(null); - } - createLatLng(lat: number, lng: number): L.LatLng { return L.latLng(lat, lng); } @@ -441,7 +427,7 @@ export default abstract class LeafletMap { } convertToCustomFormat(position: L.LatLng, offset = 0): object { - position = checkLngLat(position, this.southWest, this.northEast, offset); + position = position ? checkLngLat(position, this.southWest, this.northEast, offset) : {lat: null, lng: null} as L.LatLng; return { [this.options.latKeyName]: position.lat, @@ -455,17 +441,18 @@ export default abstract class LeafletMap { if (point.length) { return this.convertToPolygonFormat(point); } else { - return [point.lat, point.lng]; + const convertPoint = checkLngLat(point, this.southWest, this.northEast); + return [convertPoint.lat, convertPoint.lng]; } }); - } else { - return []; } + return []; } - convertPolygonToCustomFormat(expression: any[][]): object { + convertPolygonToCustomFormat(expression: any[][]): {[key: string]: any} { + const coordinate = expression ? this.convertToPolygonFormat(expression) : null; return { - [this.options.polygonKeyName] : this.convertToPolygonFormat(expression) + [this.options.polygonKeyName]: coordinate }; } @@ -474,16 +461,25 @@ export default abstract class LeafletMap { this.showPolygon = showPolygon; if (this.map) { const data = this.ctx.data; - const formattedData = parseData(this.ctx.data); + const formattedData = parseData(data); if (drawRoutes) { - this.updatePolylines(parseArray(data), false); + const polyData = parseArray(data); + this.updatePolylines(polyData, formattedData, false); } if (showPolygon) { this.updatePolygons(formattedData, false); } this.updateMarkers(formattedData, false); this.updateBoundsInternal(); - if (this.options.draggableMarker || this.options.editablePolygon) { + if (this.options.draggableMarker) { + const foundEntityWithoutLocation = formattedData.some(mData => !this.convertPosition(mData)); + this.map.pm.Toolbar.setButtonDisabled('tbMarker', !foundEntityWithoutLocation); + this.datasources = formattedData; + } + if (this.editPolygons) { + const foundEntityWithoutPolygon = formattedData.some(pData => !this.isValidPolygonPosition(pData)); + this.map.pm.Toolbar.setButtonDisabled('tbPolygon', !foundEntityWithoutPolygon); + this.map.pm.Toolbar.setButtonDisabled('tbRectangle', !foundEntityWithoutPolygon); this.datasources = formattedData; } } else { @@ -576,10 +572,10 @@ export default abstract class LeafletMap { } dragMarker = (e, data = {} as FormattedData) => { - if (e.type !== 'dragend') { + if (e === undefined) { return; } - this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) }).subscribe(); + this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); } private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, @@ -633,7 +629,8 @@ export default abstract class LeafletMap { return polygon; } - updatePoints(pointsData: FormattedData[][], getTooltip: (point: FormattedData) => string) { + updatePoints(pointsData: FormattedData[][], + getTooltip: (point: FormattedData, points: FormattedData[]) => string) { if (pointsData.length) { if (this.points) { this.map.removeLayer(this.points); @@ -642,21 +639,25 @@ export default abstract class LeafletMap { } let pointColor = this.options.pointColor; for (const pointsList of pointsData) { - pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { - if (this.options.useColorPointFunction) { - pointColor = safeExecute(this.options.colorPointFunction, [data, pointsData, data.dsIndex]); - } - const point = L.circleMarker(this.convertPosition(data), { - color: pointColor, - radius: this.options.pointSize - }); - if (!this.options.pointTooltipOnRightPanel) { - point.on('click', () => getTooltip(data)); - } else { - createTooltip(point, this.options, data.$datasource, getTooltip(data)); + for (let tsIndex = 0; tsIndex < pointsList.length; tsIndex++) { + const pdata = pointsList[tsIndex]; + if (!!this.convertPosition(pdata)) { + const dsData = pointsData.map(ds => ds[tsIndex]); + if (this.options.useColorPointFunction) { + pointColor = safeExecute(this.options.colorPointFunction, [pdata, dsData, pdata.dsIndex]); + } + const point = L.circleMarker(this.convertPosition(pdata), { + color: pointColor, + radius: this.options.pointSize + }); + if (!this.options.pointTooltipOnRightPanel) { + point.on('click', () => getTooltip(pdata, dsData)); + } else { + createTooltip(point, this.options, pdata.$datasource, getTooltip(pdata, dsData)); + } + this.points.addLayer(point); } - this.points.addLayer(point); - }); + } } if (pointsData.length) { this.map.addLayer(this.points); @@ -665,15 +666,15 @@ export default abstract class LeafletMap { // Polyline - updatePolylines(polyData: FormattedData[][], updateBounds = true, activePolyline?: FormattedData) { + updatePolylines(polyData: FormattedData[][], dsData: FormattedData[], updateBounds = true) { const keys: string[] = []; - polyData.forEach((dataSource: FormattedData[]) => { - const data = activePolyline || dataSource[0]; - if (dataSource.length && data.entityName === dataSource[0].entityName) { + polyData.forEach((tsData: FormattedData[], index) => { + const data = dsData[index]; + if (tsData.length && data.entityName === tsData[0].entityName) { if (this.polylines.get(data.entityName)) { - this.updatePolyline(data, dataSource, this.options, updateBounds); + this.updatePolyline(data, tsData, dsData, this.options, updateBounds); } else { - this.createPolyline(data, dataSource, this.options, updateBounds); + this.createPolyline(data, tsData, dsData, this.options, updateBounds); } keys.push(data.entityName); } @@ -689,9 +690,9 @@ export default abstract class LeafletMap { }); } - createPolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { + createPolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: PolylineSettings, updateBounds = true) { const poly = new Polyline(this.map, - dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); + tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); if (updateBounds) { const bounds = poly.leafletPoly.getBounds(); this.fitBounds(bounds); @@ -699,10 +700,10 @@ export default abstract class LeafletMap { this.polylines.set(data.entityName, poly); } - updatePolyline(data: FormattedData, dataSources: FormattedData[], settings: PolylineSettings, updateBounds = true) { + updatePolyline(data: FormattedData, tsData: FormattedData[], dsData: FormattedData[], settings: PolylineSettings, updateBounds = true) { const poly = this.polylines.get(data.entityName); const oldBounds = poly.leafletPoly.getBounds(); - poly.updatePolyline(dataSources.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); + poly.updatePolyline(tsData.map(el => this.convertPosition(el)).filter(el => !!el), data, dsData, settings); const newBounds = poly.leafletPoly.getBounds(); if (updateBounds && oldBounds.toBBoxString() !== newBounds.toBBoxString()) { this.fitBounds(newBounds); @@ -722,11 +723,15 @@ export default abstract class LeafletMap { // Polygon + isValidPolygonPosition(data: FormattedData): boolean { + return data && isDefinedAndNotNull(data[this.options.polygonKeyName]) && !isEmptyStr(data[this.options.polygonKeyName]); + } + updatePolygons(polyData: FormattedData[], updateBounds = true) { const keys: string[] = []; this.polygonsData = deepClone(polyData); polyData.forEach((data: FormattedData) => { - if (data && isDefinedAndNotNull(data[this.options.polygonKeyName]) && !isEmptyStr(data[this.options.polygonKeyName])) { + if (this.isValidPolygonPosition(data)) { if (isString((data[this.options.polygonKeyName]))) { data[this.options.polygonKeyName] = JSON.parse(data[this.options.polygonKeyName]); } @@ -752,15 +757,14 @@ export default abstract class LeafletMap { } dragPolygonVertex = (e?, data = {} as FormattedData) => { - if (e === undefined || (e.type !== 'editable:vertex:dragend' && e.type !== 'editable:vertex:deleted')) { + if (e === undefined) { return; } - if (this.options.provider !== MapProviders.image) { - for (const key of Object.keys(e.layer._latlngs[0])) { - e.layer._latlngs[0][key] = checkLngLat(e.layer._latlngs[0][key], this.southWest, this.northEast); - } + let coordinates = e.layer.getLatLngs(); + if (coordinates.length === 1) { + coordinates = coordinates[0]; } - this.savePolygonLocation({ ...data, ...this.convertPolygonToCustomFormat(e.layer._latlngs) }).subscribe(); + this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); } createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index cbf5883659..5b99b792e9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -57,6 +57,7 @@ export type MapSettings = { showCoverageOnHover: boolean, animate: boolean, maxClusterRadius: number, + spiderfyOnMaxZoom: boolean, chunkedLoading: boolean, removeOutsideVisibleBounds: boolean, useCustomProvider: boolean, @@ -178,7 +179,7 @@ export interface MapImage { update?: boolean; } -export type TripAnimationSettings = { +export interface TripAnimationSettings extends PolygonSettings { showPoints: boolean; pointColor: string; pointSize: number; @@ -203,7 +204,7 @@ export type TripAnimationSettings = { labelFunction: GenericFunction; useColorPointFunction: boolean; colorPointFunction: GenericFunction; -}; +} export type actionsHandler = ($event: Event, datasource: Datasource) => void; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 3ad10a0980..a2385b2877 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -14,42 +14,30 @@ /// limitations under the License. /// -import { - defaultSettings, - FormattedData, - hereProviders, - MapProviders, - UnitedMapSettings -} from './map-models'; +import { defaultSettings, FormattedData, hereProviders, MapProviders, UnitedMapSettings } from './map-models'; import LeafletMap from './leaflet-map'; import { commonMapSettingsSchema, mapPolygonSchema, - mapProviderSchema, markerClusteringSettingsSchema, markerClusteringSettingsSchemaLeaflet, routeMapSettingsSchema } from './schemes'; import { MapWidgetInterface, MapWidgetStaticInterface } from './map-widget.interface'; -import { - addCondition, - addGroupInfo, - addToSchema, - initSchema, - mergeSchemes -} from '@core/schema-utils'; +import { addCondition, addGroupInfo, addToSchema, initSchema, mergeSchemes } from '@core/schema-utils'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { getDefCenterPosition, getProviderSchema, parseFunction, parseWithTranslation } from './common-maps-utils'; import { Datasource, DatasourceData, JsonSettingsSchema, WidgetActionDescriptor } from '@shared/models/widget.models'; -import { EntityId } from '@shared/models/id/entity-id'; -import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; -import { AttributeService } from '@core/http/attribute.service'; import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; import { EntityDataPageLink } from '@shared/models/query/query.models'; +import { providerClass } from '@home/components/widget/lib/maps/providers'; import { isDefined } from '@core/utils'; +import L from 'leaflet'; import { forkJoin, Observable, of } from 'rxjs'; -import { providerClass } from '@home/components/widget/lib/maps/providers'; +import { AttributeService } from '@core/http/attribute.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; // @dynamic export class MapWidgetController implements MapWidgetInterface { @@ -81,8 +69,10 @@ export class MapWidgetController implements MapWidgetInterface { } parseWithTranslation.setTranslate(this.translate); this.map = new MapClass(this.ctx, $element, this.settings); - this.map.saveMarkerLocation = this.setMarkerLocation; - this.map.savePolygonLocation = this.savePolygonLocation; + (this.ctx as any).mapInstance = this.map; + this.map.saveMarkerLocation = this.setMarkerLocation.bind(this); + this.map.savePolygonLocation = this.savePolygonLocation.bind(this); + this.map.saveLocation = this.saveLocation.bind(this); this.pageLink = { page: 0, pageSize: this.settings.mapPageSize, @@ -178,111 +168,85 @@ export class MapWidgetController implements MapWidgetInterface { }, entityName, null, entityLabel); } - setMarkerLocation = (e: FormattedData, lat?: number, lng?: number) => { - const attributeService = this.ctx.$injector.get(AttributeService); - - const entityId: EntityId = { - entityType: e.$datasource.entityType, - id: e.$datasource.entityId + setMarkerLocation(e: FormattedData, lat?: number, lng?: number) { + let markerValue; + if (isDefined(lat) && isDefined(lng)) { + const point = lat != null && lng !== null ? L.latLng(lat, lng) : null; + markerValue = this.map.convertToCustomFormat(point); + } else if (this.settings.mapProvider !== MapProviders.image) { + markerValue = { + [this.settings.latKeyName]: e[this.settings.latKeyName], + [this.settings.lngKeyName]: e[this.settings.lngKeyName], }; - const attributes = []; - const timeseries = []; + } else { + markerValue = { + [this.settings.xPosKeyName]: e[this.settings.xPosKeyName], + [this.settings.yPosKeyName]: e[this.settings.yPosKeyName], + }; + } + return this.saveLocation(e, markerValue); + } - const latProperties = [this.settings.latKeyName, this.settings.xPosKeyName]; - const lngProperties = [this.settings.lngKeyName, this.settings.yPosKeyName]; - e.$datasource.dataKeys.forEach(key => { - let value; - if (latProperties.includes(key.name)) { - value = { - key: key.name, - value: isDefined(lat) ? lat : e[key.name] - }; - } else if (lngProperties.includes(key.name)) { - value = { - key: key.name, - value: isDefined(lng) ? lng : e[key.name] - }; - } - if (value) { - if (key.type === DataKeyType.attribute) { - attributes.push(value); - } - if (key.type === DataKeyType.timeseries) { - timeseries.push(value); - } - } - }); - const observables: Observable[] = []; - if (timeseries.length) { - observables.push(attributeService.saveEntityTimeseries( - entityId, - LatestTelemetry.LATEST_TELEMETRY, - timeseries - )); - } - if (attributes.length) { - observables.push(attributeService.saveEntityAttributes( - entityId, - AttributeScope.SERVER_SCOPE, - attributes - )); - } - if (observables.length) { - return forkJoin(observables); - } else { - return of(null); - } + savePolygonLocation(e: FormattedData, coordinates?: Array) { + let polygonValue; + if (isDefined(coordinates)) { + polygonValue = this.map.convertToPolygonFormat(coordinates); + } else { + polygonValue = { + [this.settings.polygonKeyName]: e[this.settings.polygonKeyName] + }; + } + return this.saveLocation(e, polygonValue); } - savePolygonLocation = (e: FormattedData, coordinates?: Array) => { - const attributeService = this.ctx.$injector.get(AttributeService); + saveLocation(e: FormattedData, values: {[key: string]: any}): Observable { + const attributeService = this.ctx.$injector.get(AttributeService); + const attributes = []; + const timeseries = []; - const entityId: EntityId = { - entityType: e.$datasource.entityType, - id: e.$datasource.entityId - }; - const attributes = []; - const timeseries = []; + const entityId: EntityId = { + entityType: e.$datasource.entityType, + id: e.$datasource.entityId + }; - const coordinatesProperties = this.settings.polygonKeyName; - e.$datasource.dataKeys.forEach(key => { - let value; - if (coordinatesProperties === key.name) { - value = { - key: key.name, - value: isDefined(coordinates) ? coordinates : e[key.name] - }; - } - if (value) { - if (key.type === DataKeyType.attribute) { - attributes.push(value); - } - if (key.type === DataKeyType.timeseries) { - timeseries.push(value); + for (const dataKeyName of Object.keys(values)) { + for (const key of e.$datasource.dataKeys) { + if (dataKeyName === key.name) { + const value = { + key: key.name, + value: values[dataKeyName] + }; + if (key.type === DataKeyType.attribute) { + attributes.push(value); + } else if (key.type === DataKeyType.timeseries) { + timeseries.push(value); + } + break; + } } } - }); - const observables: Observable[] = []; - if (timeseries.length) { - observables.push(attributeService.saveEntityTimeseries( - entityId, - LatestTelemetry.LATEST_TELEMETRY, - timeseries - )); - } - if (attributes.length) { - observables.push(attributeService.saveEntityAttributes( - entityId, - AttributeScope.SERVER_SCOPE, - attributes - )); - } - if (observables.length) { - return forkJoin(observables); - } else { - return of(null); + + const observables: Observable[] = []; + if (timeseries.length) { + observables.push(attributeService.saveEntityTimeseries( + entityId, + LatestTelemetry.LATEST_TELEMETRY, + timeseries + )); + } + if (attributes.length) { + observables.push(attributeService.saveEntityAttributes( + entityId, + AttributeScope.SERVER_SCOPE, + attributes + )); + } + if (observables.length) { + return forkJoin(observables); + } else { + return of(null); + } } - } initSettings(settings: UnitedMapSettings, isEditMap?: boolean): UnitedMapSettings { const functionParams = ['data', 'dsData', 'dsIndex']; @@ -335,6 +299,7 @@ export class MapWidgetController implements MapWidgetInterface { } destroy() { + (this.ctx as any).mapInstance = null; if (this.map) { this.map.remove(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss index 7abf1423c7..147386685c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss @@ -29,6 +29,10 @@ box-shadow: none; } -.leaflet-container{ +.leaflet-container { background-color: white; } + +.leaflet-buttons-control-button.pm-disabled { + pointer-events: none; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index cd3f19980f..5550706402 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -42,9 +42,7 @@ export class Marker { constructor(private map: LeafletMap, private location: L.LatLng, public settings: MarkerSettings, data?: FormattedData, dataSources?, onDragendListener?) { this.setDataSources(data, dataSources); - this.leafletMarker = L.marker(location, { - draggable: settings.draggableMarker - }); + this.leafletMarker = L.marker(location, {pmIgnore: !settings.draggableMarker}); this.markerOffset = [ isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5, @@ -72,8 +70,8 @@ export class Marker { }); } - if (onDragendListener) { - this.leafletMarker.on('dragend', (e) => onDragendListener(e, this.data)); + if (settings.draggableMarker && onDragendListener) { + this.leafletMarker.on('pm:dragend', (e) => onDragendListener(e, this.data)); } } @@ -199,19 +197,24 @@ export class Marker { createColoredMarkerIcon(color: tinycolor.Instance): { size: number[], icon: Icon } { return { - size: [21, 34], - icon: L.icon({ - iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color.toHex(), - iconSize: [21, 34], - iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]], - popupAnchor: [0, -34], - shadowUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_shadow', - shadowSize: [40, 37], - shadowAnchor: [12, 35] + size: [21, 34], + icon: L.icon({ + iconUrl: this.createColorIconURI(color), + iconSize: [21, 34], + iconAnchor: [21 * this.markerOffset[0], 34 * this.markerOffset[1]], + popupAnchor: [0, -34], + shadowUrl: 'assets/shadow.png', + shadowSize: [40, 37], + shadowAnchor: [12, 35] }) }; } + createColorIconURI(color: tinycolor.Instance): string { + const svg = ``; + return 'data:image/svg+xml;base64,' + btoa(svg); + } + removeMarker() { /* this.map$.subscribe(map => this.leafletMarker.addTo(map))*/ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index c7cca2a40b..d87d764d0a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -17,7 +17,6 @@ import L, { LatLngExpression, LeafletMouseEvent } from 'leaflet'; import { createTooltip } from './maps-utils'; import { functionValueCalculator, parseWithTranslation, safeExecute } from './common-maps-utils'; -import 'leaflet-editable/src/Leaflet.Editable'; import { FormattedData, PolygonSettings } from './map-models'; export class Polygon { @@ -37,14 +36,12 @@ export class Polygon { color: settings.polygonStrokeColor, weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, - opacity: settings.polygonStrokeOpacity + opacity: settings.polygonStrokeOpacity, + pmIgnore: !settings.editablePolygon }).addTo(this.map); - if (settings.editablePolygon) { - this.leafletPoly.enableEdit(this.map); - if (onDragendListener) { - this.leafletPoly.on('editable:vertex:dragend', e => onDragendListener(e, this.data)); - this.leafletPoly.on('editable:vertex:deleted', e => onDragendListener(e, this.data)); - } + + if (settings.editablePolygon && onDragendListener) { + this.leafletPoly.on('pm:edit', (e) => onDragendListener(e, this.data)); } @@ -73,13 +70,7 @@ export class Polygon { updatePolygon(data: FormattedData, dataSources: FormattedData[], settings: PolygonSettings) { this.data = data; this.dataSources = dataSources; - if (settings.editablePolygon) { - this.leafletPoly.disableEdit(); - } this.leafletPoly.setLatLngs(data[this.settings.polygonKeyName]); - if (settings.editablePolygon) { - this.leafletPoly.enableEdit(this.map); - } if (settings.showPolygonTooltip) { this.updateTooltip(this.data); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts index be319ded3a..895dc15763 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts @@ -14,7 +14,8 @@ /// limitations under the License. /// -import L, { PolylineDecoratorOptions } from 'leaflet'; +// @ts-ignore +import L, { PolylineDecorator, PolylineDecoratorOptions, Symbol } from 'leaflet'; import 'leaflet-polylinedecorator'; import { FormattedData, PolylineSettings } from './map-models'; @@ -23,7 +24,7 @@ import { functionValueCalculator } from '@home/components/widget/lib/maps/common export class Polyline { leafletPoly: L.Polyline; - polylineDecorator: L.PolylineDecorator; + polylineDecorator: PolylineDecorator; dataSources: FormattedData[]; data: FormattedData; @@ -36,7 +37,7 @@ export class Polyline { ).addTo(this.map); if (settings.usePolylineDecorator) { - this.polylineDecorator = L.polylineDecorator(this.leafletPoly, this.getDecoratorSettings(settings)).addTo(this.map); + this.polylineDecorator = new PolylineDecorator(this.leafletPoly, this.getDecoratorSettings(settings)).addTo(this.map); } } @@ -47,7 +48,7 @@ export class Polyline { offset: settings.decoratorOffset, endOffset: settings.endDecoratorOffset, repeat: settings.decoratorRepeat, - symbol: L.Symbol[settings.decoratorSymbol]({ + symbol: Symbol[settings.decoratorSymbol]({ pixelSize: settings.decoratorSymbolSize, polygon: false, pathOptions: { @@ -78,7 +79,8 @@ export class Polyline { opacity: functionValueCalculator(settings.useStrokeOpacityFunction, settings.strokeOpacityFunction, [this.data, this.dataSources, this.data.dsIndex], settings.strokeOpacity), weight: functionValueCalculator(settings.useStrokeWeightFunction, settings.strokeWeightFunction, - [this.data, this.dataSources, this.data.dsIndex], settings.strokeWeight) + [this.data, this.dataSources, this.data.dsIndex], settings.strokeWeight), + pmIgnore: true }; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts index f3369dd46c..00d9353218 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/google-map.ts @@ -38,7 +38,6 @@ export class GoogleMap extends LeafletMap { this.loadGoogle(() => { const map = L.map($container, { attributionControl: false, - editable: !!options.editablePolygon, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); (L.gridLayer as any).googleMutant({ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts index e15c897c3b..caac9a78b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts @@ -23,7 +23,6 @@ export class HEREMap extends LeafletMap { constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { super(ctx, $container, options); const map = L.map($container, { - editable: !!options.editablePolygon, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); const tileLayer = (L.tileLayer as any).provider(options.mapProviderHere || 'HERE.normalDay', options.credentials); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index 272476ea9e..be427f55cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -22,7 +22,6 @@ import { filter, map, mergeMap } from 'rxjs/operators'; import { aspectCache, calculateNewPointCoordinate, - checkLngLat, parseFunction } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; @@ -221,7 +220,6 @@ export class ImageMap extends LeafletMap { zoom: 1, crs: L.CRS.Simple, attributionControl: false, - editable: !!this.options.editablePolygon, tap: L.Browser.safari && L.Browser.mobile }); this.updateBounds(updateImage); @@ -263,7 +261,13 @@ export class ImageMap extends LeafletMap { return L.CRS.Simple.latLngToPoint(latLng, maxZoom - 1); } - convertToCustomFormat(position: L.LatLng, offset = 0, width = this.width, height = this.height): object { + convertToCustomFormat(position: L.LatLng, offset = 0, width = this.width, height = this.height): {[key: string]: any} { + if (!position) { + return { + [this.options.xPosKeyName]: null, + [this.options.yPosKeyName]: null + }; + } const point = this.latLngToPoint(position); const customX = calculateNewPointCoordinate(point.x, width); const customY = calculateNewPointCoordinate(point.y, height); @@ -279,13 +283,9 @@ export class ImageMap extends LeafletMap { point.y = height; } - const customLatLng = checkLngLat(this.pointToLatLng(point.x, point.y), this.southWest, this.northEast, offset); - return { [this.options.xPosKeyName]: customX, - [this.options.yPosKeyName]: customY, - [this.options.latKeyName]: customLatLng.lat, - [this.options.lngKeyName]: customLatLng.lng + [this.options.yPosKeyName]: customY }; } @@ -304,9 +304,10 @@ export class ImageMap extends LeafletMap { } } - convertPolygonToCustomFormat(expression: any[][]): object { + convertPolygonToCustomFormat(expression: any[][]): {[key: string]: any} { + const coordinate = expression ? this.convertToPolygonFormat(expression) : null; return { - [this.options.polygonKeyName] : this.convertToPolygonFormat(expression) + [this.options.polygonKeyName]: coordinate }; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts index 61312aab23..01eb04ea40 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/openstreet-map.ts @@ -23,7 +23,6 @@ export class OpenStreetMap extends LeafletMap { constructor(ctx: WidgetContext, $container, options: UnitedMapSettings) { super(ctx, $container, options); const map = L.map($container, { - editable: !!options.editablePolygon, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); let tileLayer; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts index bd34e24331..5e71d92e5d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/tencent-map.ts @@ -25,7 +25,6 @@ export class TencentMap extends LeafletMap { super(ctx, $container, options); const txUrl = 'http://rt{s}.map.gtimg.com/realtimerender?z={z}&x={x}&y={y}&type=vector&style=0'; const map = L.map($container, { - editable: !!options.editablePolygon, tap: L.Browser.safari && L.Browser.mobile }).setView(options?.defaultCenterPosition, options?.defaultZoomLevel || DEFAULT_ZOOM_LEVEL); const txLayer = L.tileLayer(txUrl, { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index 0287cb17d8..ff52ece929 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -431,11 +431,19 @@ export const commonMapSettingsSchema = condition: 'model.provider === "image-map"' }, 'showLabel', - 'label', - 'useLabelFunction', + { + key: 'useLabelFunction', + condition: 'model.showLabel === true' + }, + { + key: 'label', + condition: 'model.showLabel === true && model.useLabelFunction !== true' + }, { key: 'labelFunction', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/map/label_fn', + condition: 'model.showLabel === true && model.useLabelFunction === true' }, 'showTooltip', { @@ -451,17 +459,27 @@ export const commonMapSettingsSchema = value: 'hover', label: 'Show tooltip on hover' } - ] + ], + condition: 'model.showTooltip === true' + }, + { + key: 'autocloseTooltip', + condition: 'model.showTooltip === true' + }, + { + key: 'useTooltipFunction', + condition: 'model.showTooltip === true' }, - 'autocloseTooltip', { key: 'tooltipPattern', - type: 'textarea' + type: 'textarea', + condition: 'model.showTooltip === true && model.useTooltipFunction !== true' }, - 'useTooltipFunction', { key: 'tooltipFunction', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/map/tooltip_fn', + condition: 'model.showTooltip === true && model.useTooltipFunction === true' }, { key: 'markerOffsetX', @@ -474,6 +492,7 @@ export const commonMapSettingsSchema = { key: 'posFunction', type: 'javascript', + helpId: 'widget/lib/map/position_fn', condition: 'model.provider === "image-map"' }, { @@ -483,17 +502,25 @@ export const commonMapSettingsSchema = 'useColorFunction', { key: 'colorFunction', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/map/color_fn', + condition: 'model.useColorFunction === true' }, + 'useMarkerImageFunction', { key: 'markerImage', - type: 'image' + type: 'image', + condition: 'model.useMarkerImageFunction !== true' + }, + { + key: 'markerImageSize', + condition: 'model.useMarkerImageFunction !== true' }, - 'markerImageSize', - 'useMarkerImageFunction', { key: 'markerImageFunction', - type: 'javascript' + type: 'javascript', + helpId: 'widget/lib/map/marker_image_fn', + condition: 'model.useMarkerImageFunction === true' }, { key: 'markerImages', @@ -502,7 +529,8 @@ export const commonMapSettingsSchema = key: 'markerImages[]', type: 'image' } - ] + ], + condition: 'model.useMarkerImageFunction === true' } ] }; @@ -590,24 +618,36 @@ export const mapPolygonSchema = key: 'polygonColor', type: 'color' }, + 'usePolygonColorFunction', + { + key: 'polygonColorFunction', + helpId: 'widget/lib/map/polygon_color_fn', + type: 'javascript', + condition: 'model.usePolygonColorFunction === true' + }, 'polygonOpacity', { key: 'polygonStrokeColor', type: 'color' }, - 'polygonStrokeOpacity', 'polygonStrokeWeight', 'showPolygonTooltip', + 'polygonStrokeOpacity', + 'polygonStrokeWeight', + 'showPolygonTooltip', { - key: 'polygonTooltipPattern', - type: 'textarea' - }, 'usePolygonTooltipFunction', { - key: 'polygonTooltipFunction', - type: 'javascript' + key: 'usePolygonTooltipFunction', + condition: 'model.showPolygonTooltip === true' }, - 'usePolygonColorFunction', { - key: 'polygonColorFunction', - type: 'javascript' + key: 'polygonTooltipPattern', + type: 'textarea', + condition: 'model.showPolygonTooltip === true && model.usePolygonTooltipFunction !== true' }, + { + key: 'polygonTooltipFunction', + helpId: 'widget/lib/map/polygon_tooltip_fn', + type: 'javascript', + condition: 'model.showPolygonTooltip === true && model.usePolygonTooltipFunction === true' + } ] }; @@ -688,6 +728,11 @@ export const markerClusteringSettingsSchemaLeaflet = type: 'number', default: 80 }, + spiderfyOnMaxZoom: { + title: 'Spiderfy at the max zoom level (to see all cluster markers)', + type: 'boolean', + default: false + }, chunkedLoading: { title: 'Use chunks for adding markers so that the page does not freeze', type: 'boolean', @@ -707,6 +752,7 @@ export const markerClusteringSettingsSchemaLeaflet = 'showCoverageOnHover', 'animate', 'maxClusterRadius', + 'spiderfyOnMaxZoom', 'chunkedLoading', 'removeOutsideVisibleBounds' ] @@ -822,11 +868,18 @@ export const pathSchema = { key: 'color', type: 'color' - }, 'useColorFunction', { + }, + 'useColorFunction', + { key: 'colorFunction', - type: 'javascript' - }, 'strokeWeight', 'strokeOpacity', - 'usePolylineDecorator', { + helpId: 'widget/lib/map/path_color_fn', + type: 'javascript', + condition: 'model.useColorFunction === true' + }, + 'strokeWeight', + 'strokeOpacity', + 'usePolylineDecorator', + { key: 'decoratorSymbol', type: 'rc-select', multiple: false, @@ -837,16 +890,22 @@ export const pathSchema = value: 'dash', label: 'Dash' }] - }, 'decoratorSymbolSize', 'useDecoratorCustomColor', { + }, + 'decoratorSymbolSize', + 'useDecoratorCustomColor', + { key: 'decoratorCustomColor', type: 'color' - }, { + }, + { key: 'decoratorOffset', type: 'textarea' - }, { + }, + { key: 'endDecoratorOffset', type: 'textarea' - }, { + }, + { key: 'decoratorRepeat', type: 'textarea' } @@ -856,7 +915,7 @@ export const pathSchema = export const pointSchema = { schema: { - title: 'Trip Animation Path Configuration', + title: 'Trip Animation Points Configuration', type: 'object', properties: { showPoints: { @@ -908,13 +967,17 @@ export const pointSchema = 'useColorPointFunction', { key: 'colorPointFunction', - type: 'javascript' + helpId: 'widget/lib/map/path_point_color_fn', + type: 'javascript', + condition: 'model.useColorPointFunction === true' }, 'pointSize', 'usePointAsAnchor', { key: 'pointAsAnchorFunction', - type: 'javascript' + helpId: 'widget/lib/map/trip_point_as_anchor_fn', + type: 'javascript', + condition: 'model.usePointAsAnchor === true' }, 'pointTooltipOnRightPanel', ] @@ -1077,36 +1140,85 @@ export const tripAnimationSchema = { }, required: [] }, - form: ['normalizationStep', 'latKeyName', 'lngKeyName', 'showLabel', 'label', 'useLabelFunction', { + form: [ + 'normalizationStep', + 'latKeyName', + 'lngKeyName', + 'showLabel', + { + key: 'useLabelFunction', + condition: 'model.showLabel === true' + }, + { + key: 'label', + condition: 'model.showLabel === true && model.useLabelFunction !== true' + }, + { key: 'labelFunction', - type: 'javascript' - }, 'showTooltip', { + type: 'javascript', + helpId: 'widget/lib/map/label_fn', + condition: 'model.showLabel === true && model.useLabelFunction === true' + }, + 'showTooltip', + { key: 'tooltipColor', - type: 'color' - }, { + type: 'color', + condition: 'model.showTooltip === true' + }, + { key: 'tooltipFontColor', - type: 'color' - }, 'tooltipOpacity', { + type: 'color', + condition: 'model.showTooltip === true' + }, + { + key: 'tooltipOpacity', + condition: 'model.showTooltip === true' + }, + { + key: 'autocloseTooltip', + condition: 'model.showTooltip === true' + }, + { + key: 'useTooltipFunction', + condition: 'model.showTooltip === true', + }, + { key: 'tooltipPattern', - type: 'textarea' - }, 'useTooltipFunction', { + type: 'textarea', + condition: 'model.showTooltip === true && model.useTooltipFunction !== true' + }, + { key: 'tooltipFunction', - type: 'javascript' - }, 'autocloseTooltip', { + type: 'javascript', + helpId: 'widget/lib/map/tooltip_fn', + condition: 'model.showTooltip === true && model.useTooltipFunction === true' + }, + 'rotationAngle', + 'useMarkerImageFunction', + { key: 'markerImage', - type: 'image' - }, 'markerImageSize', 'rotationAngle', 'useMarkerImageFunction', - { + type: 'image', + condition: 'model.useMarkerImageFunction !== true' + }, + { + key: 'markerImageSize', + condition: 'model.useMarkerImageFunction !== true' + }, + { key: 'markerImageFunction', - type: 'javascript' - }, { + type: 'javascript', + helpId: 'widget/lib/map/marker_image_fn', + condition: 'model.useMarkerImageFunction === true' + }, + { key: 'markerImages', items: [ { key: 'markerImages[]', type: 'image' } - ] + ], + condition: 'model.useMarkerImageFunction === true' }] }; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index ae94d53f85..46069c15f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -15,4 +15,4 @@ limitations under the License. --> - + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index 9cca7e4a31..fd0197996e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -96,16 +96,15 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { data: [] } ]; + } else { + initialData = []; } - let markdownText: string; - if (initialData) { - const data = parseData(initialData); - markdownText = this.settings.useMarkdownTextFunction ? - safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern; - const allData = flatData(data); - const replaceInfo = processPattern(markdownText, allData); - markdownText = fillPattern(markdownText, replaceInfo, allData); - } + const data = parseData(initialData); + let markdownText = this.settings.useMarkdownTextFunction ? + safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern; + const allData = flatData(data); + const replaceInfo = processPattern(markdownText, allData); + markdownText = fillPattern(markdownText, replaceInfo, allData); if (this.markdownText !== markdownText) { this.markdownText = this.utils.customTranslation(markdownText, markdownText); this.cd.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html index 2fa82f622d..ce0bdc4acd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html @@ -104,6 +104,24 @@
+
+ + {{key.label}} + + + {{ getCustomTranslationText(option.label ? option.label : option.value) }} + + + + {{ getErrorMessageText(key.settings, 'required') }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts index 75f41e24bf..016e04392e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts @@ -24,7 +24,7 @@ import { UtilsService } from '@core/services/utils.service'; import { TranslateService } from '@ngx-translate/core'; import { DataKey, Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models'; import { IWidgetSubscription } from '@core/api/widget-api.models'; -import { createLabelFromDatasource, isDefined, isDefinedAndNotNull, isEqual, isUndefined } from '@core/utils'; +import { createLabelFromDatasource, isDefinedAndNotNull, isEqual, isNotEmptyStr, isUndefined } from '@core/utils'; import { EntityType } from '@shared/models/entity-type.models'; import * as _moment from 'moment'; import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; @@ -41,7 +41,7 @@ type FieldAlignment = 'row' | 'column'; type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries'; type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' | 'booleanCheckbox' | 'booleanSwitch' | - 'dateTime' | 'date' | 'time'; + 'dateTime' | 'date' | 'time' | 'select'; type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly'; interface MultipleInputWidgetSettings { @@ -58,9 +58,15 @@ interface MultipleInputWidgetSettings { attributesShared?: boolean; } +interface MultipleInputWidgetSelectOption { + value: string | null; + label: string; +} + interface MultipleInputWidgetDataKeySettings { dataKeyType: MultipleInputWidgetDataKeyType; dataKeyValueType: MultipleInputWidgetDataKeyValueType; + selectOptions: MultipleInputWidgetSelectOption[]; required: boolean; isEditable: MultipleInputWidgetDataKeyEditableType; disabledOnDataKey: string; @@ -251,6 +257,14 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni } // For backward compatibility + if (dataKey.settings.dataKeyValueType === 'select') { + dataKey.settings.selectOptions.forEach((option) => { + if (option.value.toLowerCase() === 'null') { + option.value = null; + } + }); + } + source.keys.push(dataKey); }); } else { @@ -345,6 +359,9 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni case 'booleanSwitch': value = (keyData[0][1] === 'true'); break; + case 'select': + value = keyData[0][1].toString(); + break; default: value = keyData[0][1]; } @@ -449,6 +466,10 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni return messageText; } + public getCustomTranslationText(value): string { + return this.utils.customTranslation(value, value); + } + public visibleKeys(source: MultipleInputWidgetSource): MultipleInputWidgetDataKey[] { return source.keys.filter(key => !key.settings.dataKeyHidden); } @@ -472,7 +493,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni if (!this.settings.showActionButtons && !this.isSavingInProgress) { this.isSavingInProgress = true; const currentValue = this.multipleInputFormGroup.get(key.formId).value; - if (!key.settings.required || (key.settings.required && isDefined(currentValue))) { + if (!key.settings.required || (key.settings.required && isDefinedAndNotNull(currentValue) && isNotEmptyStr(currentValue.toString()))) { const dataToSave: MultipleInputWidgetSource = { datasource: source.datasource, keys: [key] diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts index c791b88589..b752d73b67 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/photo-camera-input.component.ts @@ -117,6 +117,7 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On updatePhoto = false; previewPhoto: SafeUrl; lastPhoto: SafeUrl; + datasourceDetected = false; private static hasGetUserMedia(): boolean { return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia); @@ -148,14 +149,16 @@ export class PhotoCameraInputWidgetComponent extends PageComponent implements On this.width = this.settings.maxWidth ? this.settings.maxWidth : 640; this.height = this.settings.maxHeight ? this.settings.maxWidth : 480; - - if (this.datasource.type === DatasourceType.entity) { - if (this.datasource.entityType && this.datasource.entityId) { - this.isEntityDetected = true; + this.datasourceDetected = this.ctx.datasources?.length !== 0; + if (this.datasourceDetected) { + if (this.datasource.type === DatasourceType.entity) { + if (this.datasource.entityType && this.datasource.entityId) { + this.isEntityDetected = true; + } + } + if (this.datasource.dataKeys.length) { + this.dataKeyDetected = true; } - } - if (this.datasource.dataKeys.length) { - this.dataKeyDetected = true; } this.detectAvailableDevices(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html index 48f888340a..74590433f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.html @@ -17,8 +17,6 @@ -->
-
-
entity.no-data
-
widgets.invalid-qr-code-text
-
+
entity.no-data
+
widgets.invalid-qr-code-text
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts index a7c2c46911..621a54da50 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts @@ -20,7 +20,7 @@ import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { - fillPattern, + fillPattern, flatData, parseData, parseFunction, processPattern, @@ -37,7 +37,7 @@ interface QrCodeWidgetSettings { qrCodeTextFunction: string; } -type QrCodeTextFunction = (data: FormattedData) => string; +type QrCodeTextFunction = (data: FormattedData[]) => string; @Component({ selector: 'tb-qrcode-widget', @@ -54,7 +54,6 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte qrCodeText: string; invalidQrCodeText = false; - datasourceDetected = false; private viewInited: boolean; private scheduleUpdateCanvas: boolean; @@ -97,17 +96,14 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte } ]; } else { - this.datasourceDetected = false; - } - if (initialData) { - const data = parseData(initialData); - const dataSourceData = data[0]; - const pattern = this.settings.useQrCodeTextFunction ? - safeExecute(this.qrCodeTextFunction, [dataSourceData]) : this.settings.qrCodeTextPattern; - const replaceInfo = processPattern(pattern, dataSourceData); - qrCodeText = fillPattern(pattern, replaceInfo, dataSourceData); - this.datasourceDetected = true; + initialData = []; } + const data = parseData(initialData); + const pattern = this.settings.useQrCodeTextFunction ? + safeExecute(this.qrCodeTextFunction, [data]) : this.settings.qrCodeTextPattern; + const allData = flatData(data); + const replaceInfo = processPattern(pattern, allData); + qrCodeText = fillPattern(pattern, replaceInfo, allData); this.updateQrCodeText(qrCodeText); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss index b6b1c45e27..2c789bebbd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/led-indicator.component.scss @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "node_modules/compass-sass-mixins/lib/compass"; $error-height: 14px !default; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss index 96cdcadeff..7931b05131 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/round-switch.component.scss @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "node_modules/compass-sass-mixins/lib/compass"; $error-height: 14px !default; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index c18f467703..f90c287111 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -23,6 +23,8 @@ import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/ import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { FormattedData } from '@home/components/widget/lib/maps/map-models'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; const tinycolor = tinycolor_; @@ -352,6 +354,14 @@ function filterTableCellButtonAction(widgetContext: WidgetContext, } } +export function noDataMessage(noDataDisplayMessage: string, defaultMessage: string, + utils: UtilsService, translate: TranslateService): string { + if (isNotEmptyStr(noDataDisplayMessage)) { + return utils.customTranslation(noDataDisplayMessage, noDataDisplayMessage); + } + return translate.instant(defaultMessage); +} + export function constructTableCssString(widgetConfig: WidgetConfig): string { const origColor = widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; const origBackgroundColor = widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 099820cfec..8c3b9084b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -104,7 +104,7 @@ widget.no-data-found + class="no-data-found">{{ noDataDisplayMessageText }} = []; @@ -147,6 +149,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private rowStylesInfo: RowStyleInfo; private subscriptions: Subscription[] = []; + private widgetTimewindowChanged$: Subscription; private searchAction: WidgetAction = { name: 'action.search', @@ -176,6 +179,25 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.datasources = this.ctx.datasources; this.initialize(); this.ctx.updateWidgetParams(); + + if (this.displayPagination) { + this.widgetTimewindowChanged$ = this.ctx.defaultSubscription.widgetTimewindowChanged$.subscribe( + () => { + this.sources.forEach((source) => { + if (this.displayPagination) { + source.pageLink.page = 0; + } + }); + } + ); + } + } + + ngOnDestroy(): void { + if (this.widgetTimewindowChanged$) { + this.widgetTimewindowChanged$.unsubscribe(); + this.widgetTimewindowChanged$ = null; + } } ngAfterViewInit(): void { @@ -231,6 +253,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + this.noDataDisplayMessageText = + noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widget.no-data-found', this.utils, this.translate); + let cssString = constructTableCssString(this.widgetConfig); const origBackgroundColor = this.widgetConfig.backgroundColor || 'rgb(255, 255, 255)'; diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html index b98a00263b..cd539a4a50 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html @@ -16,9 +16,7 @@ -->
-
- {{label}} -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts index 2581da88ad..dfa630b3ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -29,11 +29,17 @@ import { } from '@angular/core'; import { FormattedData, MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models'; import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils'; -import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '@home/components/widget/lib/maps/schemes'; -import { DomSanitizer } from '@angular/platform-browser'; +import { + mapPolygonSchema, + pathSchema, + pointSchema, + tripAnimationSchema +} from '@home/components/widget/lib/maps/schemes'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { - findAngle, getProviderSchema, + findAngle, + getProviderSchema, getRatio, interpolateOnLineSegment, parseArray, @@ -43,7 +49,7 @@ import { } from '@home/components/widget/lib/maps/common-maps-utils'; import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models'; import moment from 'moment'; -import { isUndefined } from '@core/utils'; +import { isDefined, isUndefined } from '@core/utils'; import { ResizeObserver } from '@juggle/resize-observer'; import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface'; @@ -70,13 +76,14 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy mapWidget: MapWidgetInterface; historicalData: FormattedData[][]; normalizationStep: number; - interpolatedTimeData = []; + interpolatedTimeData: {[time: number]: FormattedData}[] = []; + formattedInterpolatedTimeData: FormattedData[][] = []; widgetConfig: WidgetConfig; settings: TripAnimationSettings; mainTooltips = []; visibleTooltip = false; activeTrip: FormattedData; - label: string; + label: SafeHtml; minTime: number; maxTime: number; anchors: number[] = []; @@ -93,7 +100,10 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy addGroupInfo(schema, 'Path Settings'); addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints'])); addGroupInfo(schema, 'Path Points Settings'); - addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); + const mapPolygonSchemaWithoutEdit = mapPolygonSchema; + delete mapPolygonSchemaWithoutEdit.schema.properties.editablePolygon; + mapPolygonSchemaWithoutEdit.form.splice(mapPolygonSchemaWithoutEdit.form.indexOf('editablePolygon'), 1); + addToSchema(schema, addCondition(mapPolygonSchemaWithoutEdit, 'model.showPolygon === true', ['showPolygon'])); addGroupInfo(schema, 'Polygon Settings'); return schema; } @@ -108,6 +118,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy rotationAngle: 0 }; this.settings = { ...settings, ...this.ctx.settings }; + this.settings.editablePolygon = false; this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor; this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']); this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']); @@ -116,10 +127,17 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy this.normalizationStep = this.settings.normalizationStep; const subscription = this.ctx.defaultSubscription; subscription.callbacks.onDataUpdated = () => { - this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length); + this.historicalData = parseArray(this.ctx.data).map(item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); + this.interpolatedTimeData.length = 0; + this.formattedInterpolatedTimeData.length = 0; if (this.historicalData.length) { + const prevMinTime = this.minTime; + const prevMaxTime = this.maxTime; this.calculateIntervals(); - this.timeUpdated(this.minTime); + const currentTime = this.calculateCurrentTime(prevMinTime, prevMaxTime); + if (currentTime !== this.currentTime) { + this.timeUpdated(currentTime); + } } this.mapWidget.map.map?.invalidateSize(); this.cd.detectChanges(); @@ -146,6 +164,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy timeUpdated(time: number) { this.currentTime = time; + // get point for each datasource associated with time const currentPosition = this.interpolatedTimeData .map(dataSource => dataSource[time]); for (let j = 0; j < this.interpolatedTimeData.length; j++) { @@ -171,20 +190,20 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time); } } - this.calcLabel(); + this.calcLabel(currentPosition); this.calcMainTooltip(currentPosition); if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) { - const formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds)); - this.mapWidget.map.updatePolylines(formattedInterpolatedTimeData, true); + this.mapWidget.map.updatePolylines(this.formattedInterpolatedTimeData, currentPosition, true); if (this.settings.showPolygon) { - this.mapWidget.map.updatePolygons(this.interpolatedTimeData); + this.mapWidget.map.updatePolygons(currentPosition); } if (this.settings.showPoints) { - this.mapWidget.map.updatePoints(formattedInterpolatedTimeData.map(ds => _.union(ds)), this.calcTooltip); + this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip); } this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => { this.activeTrip = trip; this.timeUpdated(this.currentTime); + this.cd.markForCheck(); }); } } @@ -210,46 +229,55 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } calculateIntervals() { + let minTime = Infinity; + let maxTime = -Infinity; + this.historicalData.forEach((dataSource) => { + minTime = Math.min(dataSource[0].time, minTime); + maxTime = Math.max(dataSource[dataSource.length - 1].time, maxTime); + }); + this.minTime = minTime; + this.maxTime = maxTime; this.historicalData.forEach((dataSource, index) => { - this.minTime = dataSource[0]?.time || Infinity; - this.maxTime = dataSource[dataSource.length - 1]?.time || -Infinity; this.interpolatedTimeData[index] = this.interpolateArray(dataSource); }); + this.formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds)); if (!this.activeTrip) { this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0]; } if (this.useAnchors) { const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]); this.anchors = anchorDate - .filter((data: [string, FormattedData]) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], anchorDate, data[1].dsIndex])) + .filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], + this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex])) .map(data => parseInt(data[0], 10)); } } - calcTooltip = (point: FormattedData): string => { + calcTooltip = (point: FormattedData, points: FormattedData[]): string => { const data = point ? point : this.activeTrip; const tooltipPattern: string = this.settings.useTooltipFunction ? - safeExecute(this.settings.tooltipFunction, [data, this.historicalData, point.dsIndex]) : this.settings.tooltipPattern; + safeExecute(this.settings.tooltipFunction, + [data, points, point.dsIndex]) : this.settings.tooltipPattern; return parseWithTranslation.parseTemplate(tooltipPattern, data, true); } private calcMainTooltip(points: FormattedData[]): void { const tooltips = []; for (const point of points) { - tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point))); + tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point, points))); } this.mainTooltips = tooltips; } - calcLabel() { - const data = this.activeTrip; + calcLabel(points: FormattedData[]) { + const data = points[this.activeTrip.dsIndex]; const labelText: string = this.settings.useLabelFunction ? - safeExecute(this.settings.labelFunction, [data, this.historicalData, data.dsIndex]) : this.settings.label; - this.label = (parseWithTranslation.parseTemplate(labelText, data, true)); + safeExecute(this.settings.labelFunction, [data, points, data.dsIndex]) : this.settings.label; + this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true)); } - interpolateArray(originData: FormattedData[]) { - const result = {}; + private interpolateArray(originData: FormattedData[]): {[time: number]: FormattedData} { + const result: {[time: number]: FormattedData} = {}; const latKeyName = this.settings.latKeyName; const lngKeyName = this.settings.lngKeyName; for (const data of originData) { @@ -264,10 +292,57 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy } const timeStamp = Object.keys(result); for (let i = 0; i < timeStamp.length - 1; i++) { + if (isUndefined(result[timeStamp[i + 1]][latKeyName]) || isUndefined(result[timeStamp[i + 1]][lngKeyName])) { + for (let j = i + 2; j < timeStamp.length - 1; j++) { + if (isDefined(result[timeStamp[j]][latKeyName]) || isDefined(result[timeStamp[j]][lngKeyName])) { + const ratio = getRatio(Number(timeStamp[i]), Number(timeStamp[j]), Number(timeStamp[i + 1])); + result[timeStamp[i + 1]] = { + ...interpolateOnLineSegment(result[timeStamp[i]], result[timeStamp[j]], latKeyName, lngKeyName, ratio), + ...result[timeStamp[i + 1]], + }; + break; + } + } + } result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName); } return result; } + + private calculateCurrentTime(minTime: number, maxTime: number): number { + if (minTime !== this.minTime || maxTime !== this.maxTime) { + if (this.minTime >= this.currentTime || isUndefined(this.currentTime)) { + return this.minTime; + } else if (this.maxTime <= this.currentTime) { + return this.maxTime; + } else { + return this.minTime + Math.ceil((this.currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep; + } + } + return this.currentTime; + } + + private clearIncorrectFirsLastDatapoint(dataSource: FormattedData[]): FormattedData[] { + const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate); + if (firstHistoricalDataIndexCoordinate === -1) { + return []; + } + let lastIndex = dataSource.length - 1; + for (lastIndex; lastIndex > 0; lastIndex--) { + if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) { + lastIndex++; + break; + } + } + if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) { + return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex); + } + return dataSource; + } + + private findFirstHistoricalDataIndexCoordinate = (item: FormattedData): boolean => { + return isDefined(item[this.settings.latKeyName]) && isDefined(item[this.settings.lngKeyName]); + } } export let TbTripAnimationWidget = TripAnimationComponent; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index 7ddcaa0764..90b3087ced 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts @@ -44,6 +44,7 @@ import { SharedModule } from '@shared/shared.module'; import { MODULES_MAP } from '@shared/public-api'; import * as tinycolor_ from 'tinycolor2'; import moment from 'moment'; +import { IModulesMap } from '@modules/common/modules-map.models'; const tinycolor = tinycolor_; @@ -64,7 +65,7 @@ export class WidgetComponentService { private editingWidgetType: WidgetType; constructor(@Inject(WINDOW) private window: Window, - @Optional() @Inject(MODULES_MAP) private modulesMap: {[key: string]: any}, + @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap, private dynamicComponentFactoryService: DynamicComponentFactoryService, private widgetService: WidgetService, private utils: UtilsService, @@ -491,6 +492,9 @@ export class WidgetComponentService { if (isUndefined(result.typeParameters.dataKeysOptional)) { result.typeParameters.dataKeysOptional = false; } + if (isUndefined(result.typeParameters.datasourcesOptional)) { + result.typeParameters.datasourcesOptional = false; + } if (isUndefined(result.typeParameters.stateData)) { result.typeParameters.stateData = false; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index ed9ef23f82..ad7912cdee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -30,10 +30,10 @@ import { DateRangeNavigatorPanelComponent, DateRangeNavigatorWidgetComponent } from '@home/components/widget/lib/date-range-navigator/date-range-navigator.component'; -import { MultipleInputWidgetComponent } from './lib/multiple-input-widget.component'; -import { TripAnimationComponent } from './trip-animation/trip-animation.component'; -import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component'; -import { GatewayFormComponent } from './lib/gateway/gateway-form.component'; +import { MultipleInputWidgetComponent } from '@home/components/widget/lib/multiple-input-widget.component'; +import { TripAnimationComponent } from '@home/components/widget/trip-animation/trip-animation.component'; +import { PhotoCameraInputWidgetComponent } from '@home/components/widget/lib/photo-camera-input.component'; +import { GatewayFormComponent } from '@home/components/widget/lib/gateway/gateway-form.component'; import { ImportExportService } from '@home/components/import-export/import-export.service'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; @@ -41,6 +41,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges- import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component'; import { QrCodeWidgetComponent } from '@home/components/widget/lib/qrcode-widget.component'; import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-widget.component'; +import { SelectEntityDialogComponent } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; @NgModule({ declarations: @@ -62,7 +63,8 @@ import { MarkdownWidgetComponent } from '@home/components/widget/lib/markdown-wi NavigationCardsWidgetComponent, NavigationCardWidgetComponent, QrCodeWidgetComponent, - MarkdownWidgetComponent + MarkdownWidgetComponent, + SelectEntityDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 3be4f1705b..97542fd3cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -82,222 +82,250 @@
- - - -
widget-config.datasources
-
{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}
-
-
-
- datasource.add-datasource-prompt -
- -
- -
- widget-config.datasource-type - widget-config.datasource-parameters - -
+ + + + +
widget-config.datasources
+
{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}
+
+
+
+ datasource.add-datasource-prompt
-
- - - - -
-
- - {{$index + 1}}. -
-
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - - - - - - - - - - - - - + +
+ +
+ widget-config.datasource-type + widget-config.datasource-parameters + +
+
+
+ + + + +
+
+ + {{$index + 1}}. +
+
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + + + + + + + + + + + + + + +
+ +
- - -
- + +
-
- - + + +
+
+
+
- -
- -
-
- - - - {{ 'widget-config.target-device' | translate }} - - -
- - -
-
- - - - {{ 'widget-config.alarm-source' | translate }} - - -
-
- - - - {{ datasourceTypesTranslations.get(datasourceType) | translate }} - - - -
- - - - - - + + + + + {{ 'widget-config.target-device' | translate }} + + +
+ + +
+
+ + + + {{ 'widget-config.alarm-source' | translate }} + + +
+
+ + + + {{ datasourceTypesTranslations.get(datasourceType) | translate }} + + + +
+ + + + + + +
+ +
- - -
-
-
+
+
+ + + widget-config.data-settings + + +
+ + widget-config.units + + + + widget-config.decimals + + +
+
+ + widget-config.no-data-display-message + + +
+
+
+ -
-
- widget-config.general-settings -
-
- +
+
+
+ widget-config.title + + {{ 'widget-config.display-title' | translate }} + +
+ widget-config.title @@ -306,130 +334,138 @@
-
- -
-
-
-
- +
+ widget-config.title-icon + {{ 'widget-config.display-icon' | translate }} - - - -
-
- - - - widget-config.icon-size - - -
-
-
-
- - {{ 'widget-config.display-title' | translate }} - - - {{ 'widget-config.drop-shadow' | translate }} - - - {{ 'widget-config.enable-fullscreen' | translate }} - -
-
- -
-
-
-
- - - - -
-
- - widget-config.padding - - - - widget-config.margin - - + +
+ + + + + + widget-config.icon-size + + +
+ + + + + widget-config.advanced-settings + + + + + + + +
+ widget-config.widget-style +
+
+ + + + +
+
+ + widget-config.padding + + + + widget-config.margin + + +
-
-
- - widget-config.units - - - - widget-config.decimals - - -
-
- - {{ 'widget-config.display-legend' | translate }} - -
- - -
-
-
-
- widget-config.mobile-mode-settings -
- - {{ 'widget-config.mobile-hide' | translate }} - - - widget-config.order - - - - widget-config.height - - -
+ + {{ 'widget-config.drop-shadow' | translate }} + + + {{ 'widget-config.enable-fullscreen' | translate }} + + + + + widget-config.advanced-settings + + + + + + + +
+ widget-config.legend + + + + + {{ 'widget-config.display-legend' | translate }} + + + + widget-config.advanced-settings + + + + + + +
+
+ widget-config.mobile-mode-settings + + + + + {{ 'widget-config.mobile-hide' | translate }} + + + + widget-config.advanced-settings + + + +
+ + widget-config.order + + + + widget-config.height + + +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index 6e1e1cabdf..c29afa5629 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -17,9 +17,6 @@ :host { .tb-widget-config { - .tb-advanced-widget-config { - height: 100%; - } .tb-advanced-widget-config { height: 100%; } @@ -69,6 +66,28 @@ padding-left: 8px; } } + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + } + .fields-group-slider { + padding: 0; + legend { + margin-left: 16px; + } + .tb-settings { + padding: 0 16px 8px; + } + } + .tb-widget-style { + margin-top: 16px; + } } } @@ -94,6 +113,36 @@ white-space: normal; } .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + .mat-content { + overflow: visible; + } + .mat-expansion-panel-header { + padding: 0; + &:hover { + background: none; + } + .mat-expansion-indicator { + padding: 2px; + } + } + .mat-expansion-panel-header-description { + align-items: center; + } + .mat-expansion-panel-body{ + padding: 0; + } + .tb-json-object-panel { + margin: 0 0 8px; + } + .mat-checkbox-layout { + margin: 5px 0; + } + .mat-checkbox-inner-container { + margin-right: 12px; + } + } &.tb-datasources { &.mat-expanded { overflow: visible; @@ -152,5 +201,17 @@ } } } + .mat-slide { + margin: 8px 0; + } + .slide-block { + display: block; + &:not(:last-child) { + margin-bottom: 8px; + } + } + .mat-slide-toggle-content { + white-space: normal; + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 4c0f2ad397..7d779a3a6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -209,14 +209,32 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont titleStyle: [null, []], units: [null, []], decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], + noDataDisplayMessage: [null, []], showLegend: [null, []], legendConfig: [null, []] }); + this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => { + if (value) { + this.widgetSettings.get('titleStyle').enable({emitEvent: false}); + this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); + } else { + this.widgetSettings.get('titleStyle').disable({emitEvent: false}); + this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').patchValue(false); + this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); + } + }); + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { if (value) { this.widgetSettings.get('titleIcon').enable({emitEvent: false}); + this.widgetSettings.get('iconColor').enable({emitEvent: false}); + this.widgetSettings.get('iconSize').enable({emitEvent: false}); } else { this.widgetSettings.get('titleIcon').disable({emitEvent: false}); + this.widgetSettings.get('iconColor').disable({emitEvent: false}); + this.widgetSettings.get('iconSize').disable({emitEvent: false}); } }); this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { @@ -236,6 +254,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont }); } + ngOnDestroy(): void { + this.removeChangeSubscriptions(); + } + private removeChangeSubscriptions() { if (this.dataSettingsChangesSubscription) { this.dataSettingsChangesSubscription.unsubscribe(); @@ -376,7 +398,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', - showTitle: config.showTitle, + showTitle: isDefined(config.showTitle) ? config.showTitle : false, dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, backgroundColor: config.backgroundColor, @@ -390,17 +412,32 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont }, units: config.units, decimals: config.decimals, + noDataDisplayMessage: isDefined(config.noDataDisplayMessage) ? config.noDataDisplayMessage : '', showLegend: isDefined(config.showLegend) ? config.showLegend : this.widgetType === widgetType.timeseries, legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType) }, {emitEvent: false} ); + const showTitle: boolean = this.widgetSettings.get('showTitle').value; + if (showTitle) { + this.widgetSettings.get('titleTooltip').enable({emitEvent: false}); + this.widgetSettings.get('titleStyle').enable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').enable({emitEvent: false}); + } else { + this.widgetSettings.get('titleTooltip').disable({emitEvent: false}); + this.widgetSettings.get('titleStyle').disable({emitEvent: false}); + this.widgetSettings.get('showTitleIcon').disable({emitEvent: false}); + } const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; if (showTitleIcon) { this.widgetSettings.get('titleIcon').enable({emitEvent: false}); + this.widgetSettings.get('iconColor').enable({emitEvent: false}); + this.widgetSettings.get('iconSize').enable({emitEvent: false}); } else { this.widgetSettings.get('titleIcon').disable({emitEvent: false}); + this.widgetSettings.get('iconColor').disable({emitEvent: false}); + this.widgetSettings.get('iconSize').disable({emitEvent: false}); } const showLegend: boolean = this.widgetSettings.get('showLegend').value; if (showLegend) { @@ -894,7 +931,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont }; } } else if (this.widgetType !== widgetType.static && this.modelValue.isDataEnabled) { - if (!config.datasources || !config.datasources.length) { + if (!this.modelValue.typeParameters.datasourcesOptional && (!config.datasources || !config.datasources.length)) { return { datasources: { valid: false diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html index 0b0498d562..fcaede8fc9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html @@ -39,8 +39,7 @@
widget.no-data + class="tb-absolute-fill">{{ noDataDisplayMessageText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index 941b603dcc..c1208d0241 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -107,9 +107,11 @@ import { ComponentType } from '@angular/cdk/portal'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { MobileService } from '@core/services/mobile.service'; import { DialogService } from '@core/services/dialog.service'; -import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component'; import { PopoverPlacement } from '@shared/components/popover.models'; import { TbPopoverService } from '@shared/components/popover.service'; +import { + DASHBOARD_PAGE_COMPONENT_TOKEN +} from '@home/components/tokens'; @Component({ selector: 'tb-widget', @@ -141,6 +143,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI widgetErrorData: ExceptionData; loadingData: boolean; displayNoData = false; + noDataDisplayMessageText: string; displayLegend: boolean; legendConfig: LegendConfig; @@ -182,6 +185,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI private renderer: Renderer2, private popoverService: TbPopoverService, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType, + @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType, private widgetService: WidgetService, private resources: ResourcesService, private timeService: TimeService, @@ -279,6 +283,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI this.widgetContext.servicesMap = ServicesMap; this.widgetContext.isEdit = this.isEdit; this.widgetContext.isMobile = this.isMobile; + this.widgetContext.toastTargetId = this.toastTargetId; this.widgetContext.subscriptionApi = { createSubscription: this.createSubscription.bind(this), @@ -359,6 +364,13 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI setTimeout(() => { this.dashboardWidget.updateWidgetParams(); }, 0); + + const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; + if (isNotEmptyStr(noDataDisplayMessage)) { + this.noDataDisplayMessageText = this.utils.customTranslation(noDataDisplayMessage, noDataDisplayMessage); + } else { + this.noDataDisplayMessageText = this.translate.instant('widget.no-data'); + } } ngAfterViewInit(): void { @@ -936,6 +948,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI options = { type: this.widget.type, stateData: this.typeParameters.stateData, + datasourcesOptional: this.typeParameters.datasourcesOptional, hasDataPageLink: this.typeParameters.hasDataPageLink, singleEntity: this.typeParameters.singleEntity, warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow, @@ -1344,7 +1357,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI ] }); const component = this.popoverService.displayPopover(trigger, this.renderer, - this.widgetContentContainer, DashboardPageComponent, preferredPlacement, hideOnClickOutside, + this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside, injector, { embed: true, @@ -1409,6 +1422,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI height: dialogHeight } }); + this.cd.markForCheck(); } private elementClick($event: Event) { diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index 46e83a740e..ae0be158d5 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -44,10 +44,16 @@ {{ 'device.name-required' | translate }} + + {{ 'device.name-max-length' | translate }} + device.label + + {{ 'device.label-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 09bd38d999..fdbc25107f 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -105,8 +105,8 @@ export class DeviceWizardDialogComponent extends private fb: FormBuilder) { super(store, router, dialogRef); this.deviceWizardFormGroup = this.fb.group({ - name: ['', Validators.required], - label: [''], + name: ['', [Validators.required, Validators.maxLength(255)]], + label: ['', Validators.maxLength(255)], gateway: [false], overwriteActivityTime: [false], addProfileType: [0], diff --git a/ui-ngx/src/app/modules/home/home.component.ts b/ui-ngx/src/app/modules/home/home.component.ts index 34be8cc8b2..e1ecacc2d4 100644 --- a/ui-ngx/src/app/modules/home/home.component.ts +++ b/ui-ngx/src/app/modules/home/home.component.ts @@ -25,14 +25,12 @@ import { PageComponent } from '@shared/components/page.component'; import { AppState } from '@core/core.state'; import { getCurrentAuthState, selectAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; import { MediaBreakpoints } from '@shared/models/constants'; -import * as _screenfull from 'screenfull'; +import screenfull from 'screenfull'; import { MatSidenav } from '@angular/material/sidenav'; import { AuthState } from '@core/auth/auth.models'; import { WINDOW } from '@core/services/window.service'; import { instanceOfSearchableComponent, ISearchableComponent } from '@home/models/searchable-component.models'; -const screenfull = _screenfull as _screenfull.Screenfull; - @Component({ selector: 'tb-home', templateUrl: './home.component.html', diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index f554329cc2..2f3d58e61f 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -26,11 +26,11 @@ import { Type } from '@angular/core'; import { EntityAction } from './entity-component.models'; import { HasUUID } from '@shared/models/id/has-uuid'; import { PageLink } from '@shared/models/page/page-link'; -import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; import { ActivatedRoute } from '@angular/router'; import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; import { DAY, historyInterval } from '@shared/models/time/time.models'; +import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; export type EntityBooleanFunction> = (entity: T) => boolean; export type EntityStringFunction> = (entity: T) => string; @@ -140,7 +140,7 @@ export class EntityTableConfig, P extends PageLink = P loadDataOnInit = true; onLoadAction: (route: ActivatedRoute) => void = null; - table: EntitiesTableComponent = null; + table: IEntitiesTableComponent = null; useTimePageLink = false; defaultTimewindowInterval = historyInterval(DAY); entityType: EntityType = null; diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts new file mode 100644 index 0000000000..20b1d15b32 --- /dev/null +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -0,0 +1,85 @@ +/// +/// 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 { BaseData, HasId } from '@shared/models/base-data'; +import { EntityTypeTranslation } from '@shared/models/entity-type.models'; +import { SafeHtml } from '@angular/platform-browser'; +import { PageLink } from '@shared/models/page/page-link'; +import { Timewindow } from '@shared/models/time/time.models'; +import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; +import { ElementRef, EventEmitter } from '@angular/core'; +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { + CellActionDescriptor, EntityActionTableColumn, EntityColumn, EntityTableColumn, + EntityTableConfig, + GroupActionDescriptor, + HeaderActionDescriptor +} from '@home/models/entity/entities-table-config.models'; + +export interface IEntitiesTableComponent { + entitiesTableConfig: EntityTableConfig>; + translations: EntityTypeTranslation; + headerActionDescriptors: Array; + groupActionDescriptors: Array>>; + cellActionDescriptors: Array>>; + actionColumns: Array>>; + entityColumns: Array>>; + displayedColumns: string[]; + headerCellStyleCache: Array; + cellContentCache: Array; + cellTooltipCache: Array; + cellStyleCache: Array; + selectionEnabled: boolean; + defaultPageSize: number; + displayPagination: boolean; + pageSizeOptions: number[]; + pageLink: PageLink; + textSearchMode: boolean; + timewindow: Timewindow; + dataSource: EntitiesDataSource>; + isDetailsOpen: boolean; + detailsPanelOpened: EventEmitter; + entityTableHeaderAnchor: TbAnchorComponent; + searchInputField: ElementRef; + paginator: MatPaginator; + sort: MatSort; + + addEnabled(): boolean; + clearSelection(): void; + updateData(closeDetails?: boolean): void; + onRowClick($event: Event, entity): void; + toggleEntityDetails($event: Event, entity); + addEntity($event: Event): void; + onEntityUpdated(entity: BaseData): void; + onEntityAction(action: EntityAction>): void; + deleteEntity($event: Event, entity: BaseData): void; + deleteEntities($event: Event, entities: BaseData[]): void; + onTimewindowChange(): void; + enterFilterMode(): void; + exitFilterMode(): void; + resetSortAndFilter(update?: boolean, preserveTimewindow?: boolean): void; + columnsUpdated(resetData?: boolean): void; + headerCellStyle(column: EntityColumn>): any; + clearCellCache(col: number, row: number): void; + cellContent(entity: BaseData, column: EntityColumn>, row: number): any; + cellTooltip(entity: BaseData, column: EntityColumn>, row: number): string; + cellStyle(entity: BaseData, column: EntityColumn>, row: number): any; + trackByColumnKey(index, column: EntityTableColumn>): string; + trackByEntityId(index: number, entity: BaseData): string; +} diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 59476df889..2a530c8b4e 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -230,6 +230,7 @@ export class WidgetContext { $scope: IDynamicWidgetComponent; isEdit: boolean; isMobile: boolean; + toastTargetId: string; widgetNamespace?: string; subscriptionApi?: WidgetSubscriptionApi; diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index 30fc1a66d6..7a90f69a8d 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -79,6 +79,9 @@ {{ 'admin.error-verification-url' | translate }} + + {{ 'admin.domain-name-max-length' | translate }} +
@@ -246,6 +249,9 @@ {{ 'admin.oauth2.client-id-required' | translate }} + + {{ 'admin.oauth2.client-id-max-length' | translate }} + @@ -254,6 +260,9 @@ {{ 'admin.oauth2.client-secret-required' | translate }} + + {{ 'admin.oauth2.client-secret-max-length' | translate }} +
@@ -426,17 +435,29 @@ *ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('required')"> {{ 'admin.oauth2.email-attribute-key-required' | translate }} + + {{ 'admin.oauth2.email-attribute-key-max-length' | translate }} +
admin.oauth2.first-name-attribute-key + + {{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }} + admin.oauth2.last-name-attribute-key + + {{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }} +
@@ -460,18 +481,30 @@ *ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('required')"> {{ 'admin.oauth2.tenant-name-pattern-required' | translate }} + + {{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }} +
admin.oauth2.customer-name-pattern + + {{ 'admin.oauth2.customer-name-pattern-max-length' | translate }} +
admin.oauth2.default-dashboard-name + + {{ 'admin.oauth2.default-dashboard-name-max-length' | translate }} + @@ -493,18 +526,28 @@ *ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')"> {{ 'admin.oauth2.url-pattern' | translate }} + + {{ 'admin.oauth2.url-max-length' | translate }} +
common.username + + {{ 'admin.oauth2.username-max-length' | translate }} + common.password + + {{ 'admin.oauth2.password-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index 61e001e75b..2ab97d1b44 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -155,13 +155,18 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha tenantNamePattern = {value: null, disabled: true}; } const basicGroup = this.fb.group({ - emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', Validators.required], - firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : ''], - lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : ''], + emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', + [Validators.required, Validators.maxLength(31)]], + firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : '', + Validators.maxLength(31)], + lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : '', + Validators.maxLength(31)], tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN], - tenantNamePattern: [tenantNamePattern, Validators.required], - customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null], - defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null], + tenantNamePattern: [tenantNamePattern, [Validators.required, Validators.maxLength(255)]], + customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null, + Validators.maxLength(255)], + defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null, + Validators.maxLength(255)], alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] }); @@ -178,9 +183,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): FormGroup { return this.fb.group({ - url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, [Validators.required, Validators.pattern(this.URL_REGEXP)]], - username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null], - password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null] + url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, + [Validators.required, Validators.pattern(this.URL_REGEXP), Validators.maxLength(255)]], + username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null, Validators.maxLength(255)], + password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null, Validators.maxLength(255)] }); } @@ -266,7 +272,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha private buildDomainInfoForm(domainInfo?: OAuth2DomainInfo): FormGroup { return this.fb.group({ name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ - Validators.required, + Validators.required, Validators.maxLength(255), Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]], scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] }, {validators: this.uniqueDomainValidator}); @@ -300,8 +306,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha platforms: [registration?.platforms ? registration.platforms : []], loginButtonLabel: [registration?.loginButtonLabel ? registration.loginButtonLabel : null, Validators.required], loginButtonIcon: [registration?.loginButtonIcon ? registration.loginButtonIcon : null], - clientId: [registration?.clientId ? registration.clientId : '', Validators.required], - clientSecret: [registration?.clientSecret ? registration.clientSecret : '', Validators.required], + clientId: [registration?.clientId ? registration.clientId : '', [Validators.required, Validators.maxLength(255)]], + clientSecret: [registration?.clientSecret ? registration.clientSecret : '', [Validators.required, Validators.maxLength(2048)]], accessTokenUri: [registration?.accessTokenUri ? registration.accessTokenUri : '', [Validators.required, Validators.pattern(this.URL_REGEXP)]], diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html index 1f33d63776..ff2f8a1085 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html @@ -56,6 +56,9 @@ {{ 'resource.title-required' | translate }} + + {{ 'resource.title-max-length' | translate }} + impleme super.ngOnInit(); this.entityForm.get('resourceType').valueChanges.pipe( startWith(ResourceType.LWM2M_MODEL), - pairwise(), + filter(() => this.isAdd), takeUntil(this.destroy$) - ).subscribe(([previousType, type]) => { - if (previousType === this.resourceType.LWM2M_MODEL) { - this.entityForm.get('title').setValidators(Validators.required); - this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); - } + ).subscribe((type) => { if (type === this.resourceType.LWM2M_MODEL) { - this.entityForm.get('title').clearValidators(); - this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); + this.entityForm.get('title').disable({emitEvent: false}); + this.entityForm.patchValue({title: ''}, {emitEvent: false}); + } else { + this.entityForm.get('title').enable({emitEvent: false}) } this.entityForm.patchValue({ data: null, @@ -92,11 +90,8 @@ export class ResourcesLibraryComponent extends EntityComponent impleme buildForm(entity: Resource): FormGroup { const form = this.fb.group( { - title: [entity ? entity.title : '', []], - resourceType: [{ - value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, - disabled: !this.isAdd - }, [Validators.required]], + title: [entity ? entity.title : "", [Validators.required, Validators.maxLength(255)]], + resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, [Validators.required]], fileName: [entity ? entity.fileName : null, [Validators.required]], } ); diff --git a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw index 44b0b04b02..19351e4de3 100644 --- a/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw +++ b/ui-ngx/src/app/modules/home/pages/api-usage/api_usage_json.raw @@ -1,6 +1,8 @@ { "title": "Api Usage", "image": null, + "mobileHide": false, + "mobileOrder": null, "configuration": { "description": "", "widgets": { @@ -125,7 +127,7 @@ "padding": "0", "settings": { "cardHtml": "
\n \n \n
\n
\n
\n
${title}
\n
${apiState}
\n
\n
\n
${unit}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
", - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" }, "title": "JavaScript functions", "dropShadow": true, @@ -283,7 +285,7 @@ "padding": "0", "settings": { "cardHtml": "
\n \n \n
\n
\n
\n
${title}
\n
${apiState}
\n
\n
\n
${unit}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
", - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" }, "title": "Telemetry persistence", "dropShadow": true, @@ -440,7 +442,7 @@ "color": "#666666", "padding": "0", "settings": { - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n", + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n", "cardHtml": "
\n \n \n
\n
\n
\n
${title}
\n
${apiState}
\n
\n
\n
${unit}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n \n
\n
" }, "title": "Rule Engine execution", @@ -622,7 +624,7 @@ "padding": "0", "settings": { "cardHtml": "
\n \n \n
\n
\n
\n
\n ${title}\n
\n
${apiState}
\n
\n
\n
\n
\n
{i18n:api-usage.messages}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
{i18n:api-usage.data-points}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
", - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 6px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n" + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n.card .mat-button {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 6px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n" }, "title": "Transport", "dropShadow": true, @@ -780,7 +782,7 @@ "padding": "0", "settings": { "cardHtml": "
\n \n \n
\n
\n
\n
${title}
\n
${apiState}
\n
\n
\n
${unit}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
", - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-button {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n\n" }, "title": "Alarm created", "dropShadow": true, @@ -4054,7 +4056,7 @@ "color": "#666666", "padding": "0", "settings": { - "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n.card .mat-button {\n text-transform: uppercase;\n}\n\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 6px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n", + "cardCss": ".card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 13px 13px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n.card .mat-button {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.card .mat-button-wrapper {\n pointer-events: none;\n}\n\n.card .action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 6px;\n }\n .card .mat-button {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-button {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-button {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} \n\n", "cardHtml": "
\n \n \n
\n
\n
\n
\n ${title}\n
\n
\n
\n
\n
\n
\n
{i18n:api-usage.email}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
{i18n:api-usage.sms}
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
" }, "title": "Notifications (Email/SMS)", diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html index 66b9a152c7..8d786051a7 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.html +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.html @@ -76,6 +76,9 @@ {{ 'asset.name-required' | translate }} + + {{ 'asset.name-max-length' | translate }} + asset.label + + {{ 'asset.label-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts index 0ce8f8e6e7..af4eae27de 100644 --- a/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts +++ b/ui-ngx/src/app/modules/home/pages/asset/asset.component.ts @@ -66,9 +66,9 @@ export class AssetComponent extends EntityComponent { buildForm(entity: AssetInfo): FormGroup { return this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], - type: [entity ? entity.type : null, [Validators.required]], - label: [entity ? entity.label : ''], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], + type: [entity ? entity.type : null, [Validators.required, Validators.maxLength(255)]], + label: [entity ? entity.label : '', Validators.maxLength(255)], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html index 5077950a59..28fc994477 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.html +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.html @@ -73,6 +73,9 @@ {{ 'customer.title-required' | translate }} + + {{ 'customer.title-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts index 2c6e403767..8ee70baab0 100644 --- a/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts +++ b/ui-ngx/src/app/modules/home/pages/customer/customer.component.ts @@ -58,7 +58,7 @@ export class CustomerComponent extends ContactBasedComponent { buildEntityForm(entity: Customer): FormGroup { return this.fb.group( { - title: [entity ? entity.title : '', [Validators.required]], + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], additionalInfo: this.fb.group( { description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index f9804e7bf3..0d8971395e 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -104,6 +104,9 @@ {{ 'dashboard.title-required' | translate }} + + {{ 'dashboard.title-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index 35e6daec22..f027896ffe 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -80,7 +80,7 @@ export class DashboardFormComponent extends EntityComponent { this.updateFields(entity); return this.fb.group( { - title: [entity ? entity.title : '', [Validators.required]], + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], image: [entity ? entity.image : null], mobileHide: [entity ? entity.mobileHide : false], mobileOrder: [entity ? entity.mobileOrder : null, [Validators.pattern(/^-?[0-9]+$/)]], diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index b95dbb6584..1187a34f5d 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -89,6 +89,9 @@ {{ 'device.name-required' | translate }} + + {{ 'device.name-max-length' | translate }} + device.label + + {{ 'device.label-max-length' | translate }} + { buildForm(entity: DeviceInfo): FormGroup { const form = this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], deviceProfileId: [entity ? entity.deviceProfileId : null, [Validators.required]], firmwareId: [entity ? entity.firmwareId : null], softwareId: [entity ? entity.softwareId : null], - label: [entity ? entity.label : ''], + label: [entity ? entity.label : '', [Validators.maxLength(255)]], deviceData: [entity ? entity.deviceData : null, [Validators.required]], additionalInfo: this.fb.group( { diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts index 4ffa51a6bd..32147ff475 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts @@ -18,7 +18,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { EntitiesTableComponent } from '@home/components/entity/entities-table.component'; import { Authority } from '@shared/models/authority.enum'; -import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.resolver' +import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.resolver'; import { AssetsTableConfigResolver } from '@home/pages/asset/assets-table-config.resolver'; import { DevicesTableConfigResolver } from '@home/pages/device/devices-table-config.resolver'; import { EntityViewsTableConfigResolver } from '@home/pages/entity-view/entity-views-table-config.resolver'; @@ -32,7 +32,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { RuleChainType } from '@shared/models/rule-chain.models'; import { importRuleChainBreadcumbLabelFunction, - ResolvedRuleChainMetaDataResolver, + RuleChainMetaDataResolver, ruleChainBreadcumbLabelFunction, RuleChainImportGuard, RuleChainResolver, @@ -181,7 +181,7 @@ const routes: Routes = [ }, resolve: { ruleChain: RuleChainResolver, - ruleChainMetaData: ResolvedRuleChainMetaDataResolver, + ruleChainMetaData: RuleChainMetaDataResolver, ruleNodeComponents: RuleNodeComponentsResolver, tooltipster: TooltipsterResolver } @@ -243,7 +243,7 @@ const routes: Routes = [ }, resolve: { ruleChain: RuleChainResolver, - ruleChainMetaData: ResolvedRuleChainMetaDataResolver, + ruleChainMetaData: RuleChainMetaDataResolver, ruleNodeComponents: RuleNodeComponentsResolver, tooltipster: TooltipsterResolver } diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html index 41b5363f0b..00f32ed578 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.html +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.html @@ -126,6 +126,9 @@ {{ 'edge.name-required' | translate }} + + {{ 'edge.name-max-length' | translate }} + {{ 'edge.edge-license-key-required' | translate }} + + {{ 'edge.edge-license-key-max-length' | translate }} +
@@ -150,6 +156,9 @@ {{ 'edge.cloud-endpoint-required' | translate }} + + {{ 'edge.cloud-endpoint-max-length' | translate }} +
@@ -179,6 +188,9 @@ edge.label + + {{ 'edge.label-max-length' | translate }} +
diff --git a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts index b8539df134..162bfeb463 100644 --- a/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts +++ b/ui-ngx/src/app/modules/home/pages/edge/edge.component.ts @@ -70,11 +70,11 @@ export class EdgeComponent extends EntityComponent { buildForm(entity: EdgeInfo): FormGroup { const form = this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], - type: [entity?.type ? entity.type : 'default', [Validators.required]], - label: [entity ? entity.label : ''], - cloudEndpoint: [null, [Validators.required]], - edgeLicenseKey: ['', [Validators.required]], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], + type: [entity?.type ? entity.type : 'default', [Validators.required, Validators.maxLength(255)]], + label: [entity ? entity.label : '', Validators.maxLength(255)], + cloudEndpoint: [null, [Validators.required, Validators.maxLength(255)]], + edgeLicenseKey: ['', [Validators.required, Validators.maxLength(30)]], routingKey: this.fb.control({value: entity ? entity.routingKey : null, disabled: true}), secret: this.fb.control({value: entity ? entity.secret : null, disabled: true}), additionalInfo: this.fb.group( diff --git a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html index 5ce3b05569..2b3e823327 100644 --- a/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html +++ b/ui-ngx/src/app/modules/home/pages/entity-view/entity-view.component.html @@ -76,6 +76,9 @@ {{ 'entity-view.name-required' | translate }} + + {{ 'entity-view.name-max-length' | translate }} + { buildForm(entity: EntityViewInfo): FormGroup { return this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], - type: [entity ? entity.type : null, [Validators.required]], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], + type: [entity ? entity.type : null, Validators.required], entityId: [entity ? entity.entityId : null, [Validators.required]], startTimeMs: [entity ? entity.startTimeMs : null], endTimeMs: [entity ? entity.endTimeMs : null], diff --git a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html index fa6e0428eb..a710cbb82d 100644 --- a/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html +++ b/ui-ngx/src/app/modules/home/pages/ota-update/ota-update.component.html @@ -65,6 +65,9 @@ {{ 'ota-update.title-required' | translate }} + + {{ 'ota-update.title-max-length' | translate }} + ota-update.version @@ -72,6 +75,9 @@ {{ 'ota-update.version-required' | translate }} + + {{ 'ota-update.version-max-length' | translate }} +
@@ -116,7 +122,8 @@
+ fxLayout.xs="column" fxLayout.md="column" + *ngIf="!(isAdd && this.entityForm.get('generateChecksum').value)"> ota-update.checksum-algorithm @@ -154,7 +161,8 @@ - + ota-update.direct-url-required diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html index 3ae5bfeb1c..05aafce2c5 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/add-rule-node-link-dialog.component.html @@ -34,7 +34,8 @@ required formControlName="link" [allowedLabels]="labels" - [allowCustom]="allowCustomLabels"> + [allowCustom]="allowCustomLabels" + [sourceRuleChainId]="sourceRuleChainId">
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/create-nested-rulechain-dialog.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/create-nested-rulechain-dialog.component.html new file mode 100644 index 0000000000..a8e719dbab --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/rulechain/create-nested-rulechain-dialog.component.html @@ -0,0 +1,65 @@ + +
+ +

rulenode.create-nested-rulechain

+ + +
+ + +
+ +
+ + rulechain.name + + + {{ 'rulechain.name-required' | translate }} + + + {{ 'rulechain.name-max-length' | translate }} + + +
+ + rulechain.description + + +
+
+ +
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts index acfca56c16..66be6c0f51 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/link-labels.component.ts @@ -25,7 +25,8 @@ import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material import { MatChipInputEvent, MatChipList } from '@angular/material/chips'; import { TranslateService } from '@ngx-translate/core'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { map, mergeMap, share, startWith } from 'rxjs/operators'; +import { catchError, map, mergeMap, share, startWith } from 'rxjs/operators'; +import { RuleChainService } from '@core/http/rule-chain.service'; @Component({ selector: 'tb-link-labels', @@ -68,6 +69,9 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan @Input() allowedLabels: {[label: string]: LinkLabel}; + @Input() + sourceRuleChainId: string; + linksFormGroup: FormGroup; modelValue: Array; @@ -86,7 +90,8 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan constructor(private fb: FormBuilder, public truncate: TruncatePipe, - public translate: TranslateService) { + public translate: TranslateService, + private ruleChainService: RuleChainService) { this.linksFormGroup = this.fb.group({ label: [null] }); @@ -115,7 +120,7 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan for (const propName of Object.keys(changes)) { const change = changes[propName]; if (change.currentValue !== change.previousValue) { - if (['allowCustom', 'allowedLabels'].includes(propName)) { + if (['allowCustom', 'allowedLabels', 'sourceRuleChainId'].includes(propName)) { reloadLabels = true; } } @@ -139,25 +144,50 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan this.labelsList.length = 0; this.labels.length = 0; this.modelValue = value; - for (const label of Object.keys(this.allowedLabels)) { - this.labelsList.push({name: this.allowedLabels[label].name, value: this.allowedLabels[label].value}); - } - if (value) { - value.forEach((label) => { - if (this.allowedLabels[label]) { - this.labels.push(deepClone(this.allowedLabels[label])); - } else { - this.labels.push({ - name: label, - value: label - }); - } - }); - } - if (this.chipList && this.required) { - this.chipList.errorState = this.labels.length ? false : true; + this.prepareLabelsList().subscribe((labelsList) => { + this.labelsList = labelsList; + if (value) { + value.forEach((label) => { + if (this.allowedLabels[label]) { + this.labels.push(deepClone(this.allowedLabels[label])); + } else { + this.labels.push({ + name: label, + value: label + }); + } + }); + } + if (this.chipList && this.required) { + this.chipList.errorState = !this.labels.length; + } + this.linksFormGroup.get('label').patchValue('', {emitEvent: true}); + }); + } + + prepareLabelsList(): Observable> { + const labelsList: Array = []; + if (this.sourceRuleChainId) { + return this.ruleChainService.getRuleChainOutputLabels(this.sourceRuleChainId, {ignoreErrors: true}).pipe( + map((labels) => { + for (const label of labels) { + labelsList.push({ + name: label, + value: label + }); + } + return labelsList; + }), + catchError(() => { + return of(labelsList); + }) + ); + } else { + for (const label of Object.keys(this.allowedLabels)) { + labelsList.push({name: this.allowedLabels[label].name, value: this.allowedLabels[label].value}); + } + return of(labelsList); } - this.linksFormGroup.get('label').patchValue('', {emitEvent: true}); } displayLabelFn(label?: LinkLabel): string | undefined { @@ -165,7 +195,7 @@ export class LinkLabelsComponent implements ControlValueAccessor, OnInit, OnChan } textIsNotEmpty(text: string): boolean { - return (text && text != null && text.length > 0) ? true : false; + return (text && text.length > 0); } createLinkLabel($event: Event, value: string) { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss index a0896dbacd..07f2b66d50 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-colors.scss @@ -34,7 +34,7 @@ background-color: #fbc766; } - &.tb-rule-chain-type { + &.tb-flow-type { background-color: #d6c4f1; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts index f709da687f..8fd4778abe 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -37,6 +37,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TranslateService } from '@ngx-translate/core'; import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; import { deepClone } from '@core/utils'; +import { RuleChainType } from '@shared/models/rule-chain.models'; @Component({ selector: 'tb-rule-node-config', @@ -69,6 +70,12 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On @Input() ruleNodeId: string; + @Input() + ruleChainId: string; + + @Input() + ruleChainType: RuleChainType; + nodeDefinitionValue: RuleNodeDefinition; @Input() @@ -186,6 +193,8 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On this.definedConfigComponentRef = this.definedConfigContainer.createComponent(factory); this.definedConfigComponent = this.definedConfigComponentRef.instance; this.definedConfigComponent.ruleNodeId = this.ruleNodeId; + this.definedConfigComponent.ruleChainId = this.ruleChainId; + this.definedConfigComponent.ruleChainType = this.ruleChainType; this.definedConfigComponent.configuration = this.configuration; this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => { this.updateModel(configuration); diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html index 97d03a8d73..e390175682 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
-
+
rulenode.name @@ -32,6 +32,9 @@ || ruleNodeFormGroup.get('name').hasError('pattern')"> {{ 'rulenode.name-required' | translate }} + + {{ 'rulenode.name-max-length' | translate }} + {{ 'rulenode.debug-mode' | translate }} @@ -40,6 +43,8 @@
@@ -49,19 +54,5 @@
-
- - -
- - rulenode.description - - -
-
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index d5a3fcc8a0..861f6123e1 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -75,28 +75,16 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O this.ruleNodeFormSubscription = null; } if (this.ruleNode) { - if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { - - this.ruleNodeFormGroup = this.fb.group({ - name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], - debugMode: [this.ruleNode.debugMode, []], - configuration: [this.ruleNode.configuration, [Validators.required]], - additionalInfo: this.fb.group( - { - description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''], - } - ) - }); - } else { - this.ruleNodeFormGroup = this.fb.group({ - targetRuleChainId: [this.ruleNode.targetRuleChainId, [Validators.required]], - additionalInfo: this.fb.group( - { - description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''], - } - ) - }); - } + this.ruleNodeFormGroup = this.fb.group({ + name: [this.ruleNode.name, [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*'), Validators.maxLength(255)]], + debugMode: [this.ruleNode.debugMode, []], + configuration: [this.ruleNode.configuration, [Validators.required]], + additionalInfo: this.fb.group( + { + description: [this.ruleNode.additionalInfo ? this.ruleNode.additionalInfo.description : ''], + } + ) + }); this.ruleNodeFormSubscription = this.ruleNodeFormGroup.valueChanges.subscribe(() => { this.updateRuleNode(); }); @@ -107,23 +95,8 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O private updateRuleNode() { const formValue = this.ruleNodeFormGroup.value || {}; - - if (this.ruleNode.component.type === RuleNodeType.RULE_CHAIN) { - const targetRuleChainId: string = formValue.targetRuleChainId; - if (this.ruleNode.targetRuleChainId !== targetRuleChainId && targetRuleChainId) { - this.ruleChainService.getRuleChain(targetRuleChainId).subscribe( - (ruleChain) => { - this.ruleNode.name = ruleChain.name; - Object.assign(this.ruleNode, formValue); - } - ); - } else { - Object.assign(this.ruleNode, formValue); - } - } else { - formValue.name = formValue.name.trim(); - Object.assign(this.ruleNode, formValue); - } + formValue.name = formValue.name.trim(); + Object.assign(this.ruleNode, formValue); } ngOnInit(): void { @@ -141,20 +114,19 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O } validate() { - if (this.ruleNode.component.type !== RuleNodeType.RULE_CHAIN) { - this.ruleNodeConfigComponent.validate(); - } + this.ruleNodeConfigComponent.validate(); } openRuleChain($event: Event) { if ($event) { $event.stopPropagation(); } - if (this.ruleNode.targetRuleChainId) { + const ruleChainId = this.ruleNodeFormGroup.get('configuration')?.value?.ruleChainId; + if (ruleChainId) { if (this.ruleChainType === RuleChainType.EDGE) { - this.router.navigateByUrl(`/edgeManagement/ruleChains/${this.ruleNode.targetRuleChainId}`); + this.router.navigateByUrl(`/edgeManagement/ruleChains/${ruleChainId}`); } else { - this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`); + this.router.navigateByUrl(`/ruleChains/${ruleChainId}`); } } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html index 564017b7c9..7a62dd7105 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.html @@ -22,6 +22,7 @@ formControlName="labels" [allowedLabels]="allowedLabels" [allowCustom]="allowCustom" + [sourceRuleChainId]="sourceRuleChainId" >
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts index 8ca7cf190b..7c14097979 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-link.component.ts @@ -57,6 +57,9 @@ export class RuleNodeLinkComponent implements ControlValueAccessor, OnInit { @Input() allowedLabels: {[label: string]: LinkLabel}; + @Input() + sourceRuleChainId: string; + ruleNodeLinkFormGroup: FormGroup; modelValue: FcRuleEdge; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html index 997cfb3cd5..1daaab95da 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html @@ -158,7 +158,8 @@ #tbRuleNodeLink [(ngModel)]="editingRuleNodeLink" [allowedLabels]="editingRuleNodeLinkLabels" - [allowCustom]="editingRuleNodeAllowCustomLabels"> + [allowCustom]="editingRuleNodeAllowCustomLabels" + [sourceRuleChainId]="editingRuleNodeSourceRuleChainId"> diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss index aab0c4d127..a05c80bb04 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss @@ -184,7 +184,7 @@ display: block; position: absolute; top: 11px; - right: -12px; + right: 10px; border: 1px solid #FFFFFF; border-radius: 4px; line-height: 18px; @@ -325,7 +325,7 @@ } .tb-rule-chain-context-menu { - min-width: 256px; + min-width: 320px; max-height: 404px; border-radius: 8px; margin-left: -20px; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index ea9cc54747..b240fc104b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -43,20 +43,18 @@ import { ActivatedRoute, Router } from '@angular/router'; import { inputNodeComponent, NodeConnectionInfo, - ResolvedRuleChainMetaData, RuleChain, - RuleChainConnectionInfo, RuleChainImport, RuleChainMetaData, - ruleChainNodeComponent, RuleChainType + RuleChainType } from '@shared/models/rule-chain.models'; -import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart'; import { FcRuleEdge, FcRuleNode, FcRuleNodeType, getRuleNodeHelpLink, - LinkLabel, + LinkLabel, outputNodeClazz, ruleChainNodeClazz, RuleNode, RuleNodeComponentDescriptor, RuleNodeType, @@ -75,7 +73,6 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { MatMenuTrigger } from '@angular/material/menu'; import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service'; import { Hotkey } from 'angular2-hotkeys'; -import { EntityType } from '@shared/models/entity-type.models'; import { DebugEventType, EventType } from '@shared/models/event.models'; import Timeout = NodeJS.Timeout; @@ -130,6 +127,7 @@ export class RuleChainPageComponent extends PageComponent editingRuleNodeIndex = -1; editingRuleNodeAllowCustomLabels = false; editingRuleNodeLinkLabels: {[label: string]: LinkLabel}; + editingRuleNodeSourceRuleChainId: string; @ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent; @ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent; @@ -147,7 +145,7 @@ export class RuleChainPageComponent extends PageComponent ruleNodeTypeSearch = ''; ruleChain: RuleChain; - ruleChainMetaData: ResolvedRuleChainMetaData; + ruleChainMetaData: RuleChainMetaData; ruleChainModel: FcRuleNodeModel = { nodes: [], @@ -179,16 +177,11 @@ export class RuleChainPageComponent extends PageComponent createEdge: (event, edge: FcRuleEdge) => { const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; if (sourceNode.component.type === RuleNodeType.INPUT) { - const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination) as FcRuleNode; - if (destNode.component.type === RuleNodeType.RULE_CHAIN) { - return NEVER; - } else { - const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + '')); - if (found) { - this.ruleChainCanvas.modelService.edges.delete(found); - } - return of(edge); + const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + '')); + if (found) { + this.ruleChainCanvas.modelService.edges.delete(found); } + return of(edge); } else { if (edge.label) { if (!edge.labels) { @@ -198,8 +191,9 @@ export class RuleChainPageComponent extends PageComponent } else { const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); + const sourceRuleChainId = this.ruleChainService.ruleNodeSourceRuleChainId(sourceNode.component, sourceNode.configuration); this.enableHotKeys = false; - return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe( + return this.addRuleNodeLink(edge, labels, allowCustomLabels, sourceRuleChainId).pipe( tap(() => { this.enableHotKeys = true; }), @@ -294,7 +288,7 @@ export class RuleChainPageComponent extends PageComponent if (this.isImport) { const ruleChainImport: RuleChainImport = this.itembuffer.getRuleChainImport(); this.ruleChain = ruleChainImport.ruleChain; - this.ruleChainMetaData = ruleChainImport.resolvedMetadata; + this.ruleChainMetaData = ruleChainImport.metadata; } else { this.ruleChain = this.route.snapshot.data.ruleChain; this.ruleChainMetaData = this.route.snapshot.data.ruleChainMetaData; @@ -411,6 +405,17 @@ export class RuleChainPageComponent extends PageComponent }, ['INPUT', 'SELECT', 'TEXTAREA'], this.translate.instant('rulenode.delete-selected-objects')) ); + this.hotKeys.push( + new Hotkey('ctrl+r', (event: KeyboardEvent) => { + if (this.enableHotKeys && this.canCreateNestedRuleChain()) { + event.preventDefault(); + this.createNestedRuleChain(); + return false; + } + return true; + }, ['INPUT', 'SELECT', 'TEXTAREA'], + this.translate.instant('rulenode.create-nested-rulechain')) + ); } } @@ -601,63 +606,6 @@ export class RuleChainPageComponent extends PageComponent } }); } - if (this.ruleChainMetaData.ruleChainConnections) { - const ruleChainsMap = this.ruleChainMetaData.targetRuleChainsMap; - const ruleChainNodesMap: {[ruleChainNodeId: string]: FcRuleNode} = {}; - const ruleChainEdgeMap: {[edgeKey: string]: FcRuleEdge} = {}; - this.ruleChainMetaData.ruleChainConnections.forEach((ruleChainConnection) => { - const ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id]; - if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) { - let ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId]; - if (!ruleChainNode) { - ruleChainNode = { - id: 'rule-chain-node-' + this.nextNodeID++, - name: ruleChain.name ? ruleChain.name : 'Unresolved', - targetRuleChainId: ruleChain.name ? ruleChainConnection.targetRuleChainId.id : null, - error: ruleChain.name ? undefined : this.translate.instant('rulenode.invalid-target-rulechain'), - additionalInfo: ruleChainConnection.additionalInfo, - x: Math.round(ruleChainConnection.additionalInfo.layoutX), - y: Math.round(ruleChainConnection.additionalInfo.layoutY), - component: ruleChainNodeComponent, - nodeClass: ruleNodeTypeDescriptors.get(RuleNodeType.RULE_CHAIN).nodeClass, - icon: ruleNodeTypeDescriptors.get(RuleNodeType.RULE_CHAIN).icon, - connectors: [ - { - type: FlowchartConstants.leftConnectorType, - id: (this.nextConnectorID++) + '' - } - ], - ruleChainType: this.ruleChainType - }; - ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode; - this.ruleChainModel.nodes.push(ruleChainNode); - } - const sourceNode = nodes[ruleChainConnection.fromIndex]; - if (sourceNode) { - const connectors = sourceNode.connectors.filter(connector => connector.type === FlowchartConstants.rightConnectorType); - if (connectors && connectors.length) { - const sourceId = connectors[0].id; - const destId = ruleChainNode.connectors[0].id; - const edgeKey = sourceId + '_' + destId; - let ruleChainEdge = ruleChainEdgeMap[edgeKey]; - if (!ruleChainEdge) { - ruleChainEdge = { - source: sourceId, - destination: destId, - label: ruleChainConnection.type, - labels: [ruleChainConnection.type] - }; - ruleChainEdgeMap[edgeKey] = ruleChainEdge; - this.ruleChainModel.edges.push(ruleChainEdge); - } else { - ruleChainEdge.label += ' / ' + ruleChainConnection.type; - ruleChainEdge.labels.push(ruleChainConnection.type); - } - } - } - } - }); - } if (this.ruleChainCanvas) { this.ruleChainCanvas.adjustCanvasSize(true); } @@ -747,6 +695,19 @@ export class RuleChainPageComponent extends PageComponent shortcut: 'Esc' } ); + if (this.canCreateNestedRuleChain()) { + contextInfo.menuItems.push( + { + action: () => { + this.createNestedRuleChain(); + }, + enabled: true, + value: 'rulenode.create-nested-rulechain', + icon: 'settings_ethernet', + shortcut: 'M-R' + } + ); + } contextInfo.menuItems.push( { action: () => { @@ -881,6 +842,208 @@ export class RuleChainPageComponent extends PageComponent return contextInfo; } + private canCreateNestedRuleChain(): boolean { + const selectedNodes = this.ruleChainCanvas.modelService.nodes.getSelectedNodes(); + const selectedEdges = this.ruleChainCanvas.modelService.edges.getSelectedEdges(); + if (selectedNodes.length > 1) { + const toIndexSet = new Set(); + selectedEdges.forEach((edge: FcRuleEdge) => { + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); + const fromIndex = selectedNodes.indexOf(sourceNode); + const toIndex = selectedNodes.indexOf(destNode); + if (fromIndex > -1 && toIndex > -1) { + toIndexSet.add(toIndex); + } + }); + const noInputNodes = selectedNodes.filter((node, index) => !toIndexSet.has(index)); + return noInputNodes.filter((node: FcRuleNode) => node.component.configurationDescriptor.nodeDefinition.inEnabled).length <= 1; + } + return false; + } + + private createNestedRuleChain() { + const selectedNodes = this.ruleChainCanvas.modelService.nodes.getSelectedNodes(); + const selectedEdges = this.ruleChainCanvas.modelService.edges.getSelectedEdges(); + this.dialog.open(CreateNestedRuleChainDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + ruleChainType: this.ruleChainType + } + }).afterClosed().subscribe((ruleChain) => { + if (ruleChain) { + this.ruleChainCanvas.modelService.deselectAll(); + const ruleChainMetaData: RuleChainMetaData = { + ruleChainId: ruleChain.id, + nodes: [], + connections: [] + }; + let outputEdges: FcRuleEdge[] = []; + let minX: number = null; + let maxX = 0; + let minY = null; + let maxY = 0; + + selectedNodes.forEach((node: FcRuleNode) => { + const ruleNode: RuleNode = { + type: node.component.clazz, + name: node.name, + configuration: deepClone(node.configuration), + additionalInfo: node.additionalInfo ? deepClone(node.additionalInfo) : {}, + debugMode: node.debugMode + }; + if (minX === null) { + minX = node.x; + } else { + minX = Math.min(minX, node.x); + } + if (minY === null) { + minY = node.y; + } else { + minY = Math.min(minY, node.y); + } + maxX = Math.max(maxX, node.x); + maxY = Math.max(maxY, node.y); + ruleNode.additionalInfo.layoutX = Math.round(node.x); + ruleNode.additionalInfo.layoutY = Math.round(node.y); + ruleChainMetaData.nodes.push(ruleNode); + const outputConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(node, FlowchartConstants.rightConnectorType); + outputConnectors.forEach(connector => { + const nodeOutputEdges = this.ruleChainCanvas.modelService.model.edges.filter(edge => edge.source === connector.id); + const outerEdges = nodeOutputEdges.filter(edge => { + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); + return selectedNodes.indexOf(destNode) === -1; + }); + outputEdges = outputEdges.concat(outerEdges); + }); + }); + const toIndexSet = new Set(); + selectedEdges.forEach((edge: FcRuleEdge) => { + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); + if (sourceNode.component.type !== RuleNodeType.INPUT) { + const fromIndex = selectedNodes.indexOf(sourceNode); + const toIndex = selectedNodes.indexOf(destNode); + if (fromIndex > -1 && toIndex > -1) { + const nodeConnection = { + fromIndex, + toIndex + } as NodeConnectionInfo; + edge.labels.forEach((label) => { + const newNodeConnection = deepClone(nodeConnection); + newNodeConnection.type = label; + ruleChainMetaData.connections.push(newNodeConnection); + }); + toIndexSet.add(toIndex); + } + } + }); + const noInputNodes = selectedNodes.filter((node, index) => !toIndexSet.has(index)); + const possibleInputNodes = noInputNodes.filter((node: FcRuleNode) => + node.component.configurationDescriptor.nodeDefinition.inEnabled); + let inputEdges: FcRuleEdge[] = []; + if (possibleInputNodes.length) { + const firstNode = possibleInputNodes[0]; + const inputConnectors = this.ruleChainCanvas.modelService.nodes + .getConnectorsByType(firstNode, FlowchartConstants.leftConnectorType); + if (inputConnectors.length) { + const inputConnector = inputConnectors[0]; + const nodeInputEdges = this.ruleChainCanvas.modelService.model.edges.filter(edge => edge.destination === inputConnector.id); + inputEdges = inputEdges.concat(nodeInputEdges); + } + ruleChainMetaData.firstNodeIndex = selectedNodes.indexOf(firstNode); + } + outputEdges.forEach((outputEdge) => { + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(outputEdge.source); + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(outputEdge.destination); + const outputNode: RuleNode = { + type: outputNodeClazz, + name: outputEdge.label, + configuration: {}, + additionalInfo: {}, + debugMode: false + }; + outputNode.additionalInfo.layoutX = Math.round(destNode.x); + outputNode.additionalInfo.layoutY = Math.round(destNode.y); + ruleChainMetaData.nodes.push(outputNode); + const fromIndex = selectedNodes.indexOf(sourceNode); + const toIndex = ruleChainMetaData.nodes.length - 1; + const nodeConnection = { + fromIndex, + toIndex + } as NodeConnectionInfo; + outputEdge.labels.forEach((label) => { + const newNodeConnection = deepClone(nodeConnection); + newNodeConnection.type = label; + ruleChainMetaData.connections.push(newNodeConnection); + }); + }); + const deltaX = Math.round(minX - 375); + const deltaY = Math.round(minY - 150); + ruleChainMetaData.nodes.forEach((node) => { + node.additionalInfo.layoutX -= deltaX; + node.additionalInfo.layoutY -= deltaY; + }); + this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe(() => { + const component = this.ruleChainService.getRuleNodeComponentByClazz(this.ruleChainType, ruleChainNodeClazz); + const descriptor = ruleNodeTypeDescriptors.get(component.type); + let icon = descriptor.icon; + let iconUrl = null; + if (component.configurationDescriptor.nodeDefinition.icon) { + icon = component.configurationDescriptor.nodeDefinition.icon; + } + if (component.configurationDescriptor.nodeDefinition.iconUrl) { + iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl; + } + const ruleChainNodeX = (minX + maxX) / 2; + const ruleChainNodeY = (minY + maxY) / 2; + const ruleChainInputId = (this.nextConnectorID++) + ''; + const ruleChainOutputId = (this.nextConnectorID++) + ''; + const ruleChainNode: FcRuleNode = { + name: ruleChain.name, + component, + id: 'rule-chain-node-' + this.nextNodeID++, + configuration: { + ruleChainId: ruleChain.id.id + }, + debugMode: false, + x: Math.round(ruleChainNodeX), + y: Math.round(ruleChainNodeY), + nodeClass: descriptor.nodeClass, + icon, + iconUrl, + ruleChainType: this.ruleChainType, + connectors: [ + { + type: FlowchartConstants.leftConnectorType, + id: ruleChainInputId + }, + { + type: FlowchartConstants.rightConnectorType, + id: ruleChainOutputId + } + ] + }; + this.ruleChainModel.nodes.push(ruleChainNode); + inputEdges.forEach((inputEdge) => { + inputEdge.destination = ruleChainInputId; + }); + outputEdges.forEach((outputEdge) => { + outputEdge.source = ruleChainOutputId; + outputEdge.labels = [outputEdge.label]; + }); + selectedNodes.forEach((node) => { + this.ruleChainCanvas.modelService.nodes.delete(node); + }); + this.onModelChanged(); + this.updateRuleNodesHighlight(); + }); + } + }); + } + onModelChanged() { this.isDirtyValue = true; this.validate(); @@ -918,6 +1081,8 @@ export class RuleChainPageComponent extends PageComponent this.editingRuleNode = null; this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); this.editingRuleNodeAllowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); + this.editingRuleNodeSourceRuleChainId = + this.ruleChainService.ruleNodeSourceRuleChainId(sourceNode.component, sourceNode.configuration); this.isEditingRuleNodeLink = true; this.editingRuleNodeLinkIndex = this.ruleChainModel.edges.indexOf(edge); this.editingRuleNodeLink = deepClone(edge); @@ -1093,7 +1258,7 @@ export class RuleChainPageComponent extends PageComponent const type = ruleNodeTypeDescriptors.get(ruleNodeType); this.displayTooltip(event, '
' + - '
' + + '
' + '
' + this.translate.instant(type.name) + '
' + '
' + this.translate.instant(type.details) + '
' + '
' + @@ -1104,7 +1269,7 @@ export class RuleChainPageComponent extends PageComponent displayLibNodeDescriptionTooltip(event: MouseEvent, node: FcRuleNodeType) { this.displayTooltip(event, '
' + - '
' + + '
' + '
' + node.component.name + '
' + '
' + node.component.configurationDescriptor.nodeDefinition.description + '
' + '
' + node.component.configurationDescriptor.nodeDefinition.details + '
' + @@ -1129,7 +1294,7 @@ export class RuleChainPageComponent extends PageComponent } } let tooltipContent = '
' + - '
' + + '
' + '
' + name + '
' + '
' + desc + '
'; if (details) { @@ -1189,7 +1354,7 @@ export class RuleChainPageComponent extends PageComponent resetDebugModeInAllNodes() { let changed = false; this.ruleChainModel.nodes.forEach((node) => { - if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) { + if (node.component.type !== RuleNodeType.INPUT) { changed = changed || node.debugMode; node.debugMode = false; } @@ -1223,12 +1388,11 @@ export class RuleChainPageComponent extends PageComponent const ruleChainMetaData: RuleChainMetaData = { ruleChainId: this.ruleChain.id, nodes: [], - connections: [], - ruleChainConnections: [] + connections: [] }; const nodes: FcRuleNode[] = []; this.ruleChainModel.nodes.forEach((node) => { - if (node.component.type !== RuleNodeType.INPUT && node.component.type !== RuleNodeType.RULE_CHAIN) { + if (node.component.type !== RuleNodeType.INPUT) { const ruleNode: RuleNode = { id: node.ruleNodeId, type: node.component.clazz, @@ -1253,35 +1417,19 @@ export class RuleChainPageComponent extends PageComponent const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); if (sourceNode.component.type !== RuleNodeType.INPUT) { const fromIndex = nodes.indexOf(sourceNode); - if (destNode.component.type === RuleNodeType.RULE_CHAIN) { - const ruleChainConnection = { - fromIndex, - targetRuleChainId: {entityType: EntityType.RULE_CHAIN, id: destNode.targetRuleChainId}, - additionalInfo: destNode.additionalInfo ? destNode.additionalInfo : {} - } as RuleChainConnectionInfo; - ruleChainConnection.additionalInfo.layoutX = Math.round(destNode.x); - ruleChainConnection.additionalInfo.layoutY = Math.round(destNode.y); - ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id; - edge.labels.forEach((label) => { - const newRuleChainConnection = deepClone(ruleChainConnection); - newRuleChainConnection.type = label; - ruleChainMetaData.ruleChainConnections.push(newRuleChainConnection); - }); - } else { - const toIndex = nodes.indexOf(destNode); - const nodeConnection = { - fromIndex, - toIndex - } as NodeConnectionInfo; - edge.labels.forEach((label) => { - const newNodeConnection = deepClone(nodeConnection); - newNodeConnection.type = label; - ruleChainMetaData.connections.push(newNodeConnection); - }); - } + const toIndex = nodes.indexOf(destNode); + const nodeConnection = { + fromIndex, + toIndex + } as NodeConnectionInfo; + edge.labels.forEach((label) => { + const newNodeConnection = deepClone(nodeConnection); + newNodeConnection.type = label; + ruleChainMetaData.connections.push(newNodeConnection); + }); } }); - this.ruleChainService.saveAndGetResolvedRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { + this.ruleChainService.saveRuleChainMetadata(ruleChainMetaData).subscribe((savedRuleChainMetaData) => { this.ruleChainMetaData = savedRuleChainMetaData; if (this.isImport) { this.isDirtyValue = false; @@ -1346,7 +1494,8 @@ export class RuleChainPageComponent extends PageComponent ); } - addRuleNodeLink(link: FcRuleEdge, labels: {[label: string]: LinkLabel}, allowCustomLabels: boolean): Observable { + addRuleNodeLink(link: FcRuleEdge, labels: {[label: string]: LinkLabel}, + allowCustomLabels: boolean, sourceRuleChainId: string): Observable { return this.dialog.open(AddRuleNodeLinkDialogComponent, { disableClose: true, @@ -1354,7 +1503,8 @@ export class RuleChainPageComponent extends PageComponent data: { link, labels, - allowCustomLabels + allowCustomLabels, + sourceRuleChainId } }).afterClosed(); } @@ -1384,7 +1534,7 @@ export class RuleChainPageComponent extends PageComponent } ); const content = '
' + - '
' + + '
' + '
' + node.error + '
' + '
' + '
'; @@ -1452,6 +1602,7 @@ export interface AddRuleNodeLinkDialogData { link: FcRuleEdge; labels: {[label: string]: LinkLabel}; allowCustomLabels: boolean; + sourceRuleChainId: string; } @Component({ @@ -1468,6 +1619,7 @@ export class AddRuleNodeLinkDialogComponent extends DialogComponent + implements OnInit, ErrorStateMatcher { + + createNestedRuleChainFormGroup: FormGroup; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: CreateNestedRuleChainDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private fb: FormBuilder, + private ruleChainService: RuleChainService, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + + } + + ngOnInit(): void { + this.createNestedRuleChainFormGroup = this.fb.group( + { + name: ['', [Validators.required, Validators.maxLength(255)]], + additionalInfo: this.fb.group( + { + description: [''], + } + ) + } + ); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + add(): void { + this.submitted = true; + const ruleChain = { + name: this.createNestedRuleChainFormGroup.get('name').value, + debugMode: false, + type: this.data.ruleChainType, + additionalInfo: { + description: this.createNestedRuleChainFormGroup.get('additionalInfo').get('description').value + } + } as RuleChain; + this.ruleChainService.saveRuleChain(ruleChain).subscribe( + (savedRuleChain) => { + this.dialogRef.close(savedRuleChain); + } + ); + } +} + diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts index b6ff534cf6..40a2f4cf28 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { FcModel } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FcModel } from 'ngx-flowchart'; import { FcRuleEdge, FcRuleNode, FcRuleNodeType } from '@shared/models/rule-node.models'; export interface FcRuleNodeTypeModel extends FcModel { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts index d84bdfafcb..a21b73d2f5 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-routing.module.ts @@ -32,7 +32,7 @@ import { RuleChainsTableConfigResolver } from '@modules/home/pages/rulechain/rul import { from, Observable } from 'rxjs'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { - ResolvedRuleChainMetaData, + RuleChainMetaData, RuleChain, RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainService } from '@core/http/rule-chain.service'; @@ -41,6 +41,7 @@ import { RuleNodeComponentDescriptor } from '@shared/models/rule-node.models'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ItemBufferService } from '@core/public-api'; import { MODULES_MAP } from '@shared/public-api'; +import { IModulesMap } from '@modules/common/modules-map.models'; @Injectable() export class RuleChainResolver implements Resolve { @@ -55,14 +56,14 @@ export class RuleChainResolver implements Resolve { } @Injectable() -export class ResolvedRuleChainMetaDataResolver implements Resolve { +export class RuleChainMetaDataResolver implements Resolve { constructor(private ruleChainService: RuleChainService) { } - resolve(route: ActivatedRouteSnapshot): Observable { + resolve(route: ActivatedRouteSnapshot): Observable { const ruleChainId = route.params.ruleChainId; - return this.ruleChainService.getResolvedRuleChainMetadata(ruleChainId); + return this.ruleChainService.getRuleChainMetadata(ruleChainId); } } @@ -70,7 +71,7 @@ export class ResolvedRuleChainMetaDataResolver implements Resolve> { constructor(private ruleChainService: RuleChainService, - @Optional() @Inject(MODULES_MAP) private modulesMap: {[key: string]: any}) { + @Optional() @Inject(MODULES_MAP) private modulesMap: IModulesMap) { } resolve(route: ActivatedRouteSnapshot): Observable> { @@ -159,7 +160,7 @@ const routes: Routes = [ }, resolve: { ruleChain: RuleChainResolver, - ruleChainMetaData: ResolvedRuleChainMetaDataResolver, + ruleChainMetaData: RuleChainMetaDataResolver, ruleNodeComponents: RuleNodeComponentsResolver, tooltipster: TooltipsterResolver } @@ -195,7 +196,7 @@ const routes: Routes = [ providers: [ RuleChainsTableConfigResolver, RuleChainResolver, - ResolvedRuleChainMetaDataResolver, + RuleChainMetaDataResolver, RuleNodeComponentsResolver, TooltipsterResolver, RuleChainImportGuard diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html index 66d5d6d74c..a93fac9beb 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html @@ -84,6 +84,9 @@ {{ 'rulechain.name-required' | translate }} + + {{ 'rulechain.name-max-length' | translate }} + {{ 'rulechain.debug-mode' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts index 9aa518e4c0..d38e500324 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.ts @@ -58,7 +58,7 @@ export class RuleChainComponent extends EntityComponent { buildForm(entity: RuleChain): FormGroup { return this.fb.group( { - name: [entity ? entity.name : '', [Validators.required]], + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], debugMode: [entity ? entity.debugMode : false], additionalInfo: this.fb.group( { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts index 621149c7a8..73df594f89 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain.module.ts @@ -23,11 +23,11 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m import { RuleChainTabsComponent } from '@home/pages/rulechain/rulechain-tabs.component'; import { AddRuleNodeDialogComponent, - AddRuleNodeLinkDialogComponent, + AddRuleNodeLinkDialogComponent, CreateNestedRuleChainDialogComponent, RuleChainPageComponent } from './rulechain-page.component'; import { RuleNodeComponent } from '@home/pages/rulechain/rulenode.component'; -import { FC_NODE_COMPONENT_CONFIG } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FC_NODE_COMPONENT_CONFIG } from 'ngx-flowchart'; import { RuleNodeDetailsComponent } from './rule-node-details.component'; import { RuleNodeLinkComponent } from './rule-node-link.component'; import { LinkLabelsComponent } from '@home/pages/rulechain/link-labels.component'; @@ -44,7 +44,8 @@ import { RuleNodeConfigComponent } from './rule-node-config.component'; LinkLabelsComponent, RuleNodeLinkComponent, AddRuleNodeLinkDialogComponent, - AddRuleNodeDialogComponent + AddRuleNodeDialogComponent, + CreateNestedRuleChainDialogComponent ], providers: [ { diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html index da53c21a08..3de5679cc5 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.html @@ -59,8 +59,8 @@ ×
-
-
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts index 03d5d17f55..6825f7b5ff 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulenode.component.ts @@ -16,10 +16,11 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { Component, OnInit } from '@angular/core'; -import { FcNodeComponent } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FcNodeComponent } from 'ngx-flowchart'; import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; import { Router } from '@angular/router'; import { RuleChainType } from '@app/shared/models/rule-chain.models'; +import { TranslateService } from '@ngx-translate/core'; @Component({ // tslint:disable-next-line:component-selector @@ -33,6 +34,7 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit { RuleNodeType = RuleNodeType; constructor(private sanitizer: DomSanitizer, + private translate: TranslateService, private router: Router) { super(); } @@ -48,13 +50,48 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit { if ($event) { $event.stopPropagation(); } - if (node.targetRuleChainId) { + if (node.configuration?.ruleChainId) { if (node.ruleChainType === RuleChainType.EDGE) { - this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.targetRuleChainId}`); + this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.configuration?.ruleChainId}`); } else { - this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); + this.router.navigateByUrl(`/ruleChains/${node.configuration?.ruleChainId}`); } } } + + displayOpenRuleChainTooltip($event: MouseEvent, node: FcRuleNode) { + if ($event) { + $event.stopPropagation(); + } + this.userNodeCallbacks.mouseLeave($event, node); + const tooltipContent = '
' + + '
' + + '
' + this.translate.instant('rulechain.open-rulechain') + '
'; + const element = $($event.target); + element.tooltipster( + { + theme: 'tooltipster-shadow', + delay: 100, + trigger: 'custom', + triggerOpen: { + click: false, + tap: false + }, + triggerClose: { + click: true, + tap: true, + scroll: true, + mouseleave: true + }, + side: 'top', + distance: 12, + trackOrigin: true + } + ); + const tooltip = element.tooltipster('instance'); + const contentElement = $(tooltipContent); + tooltip.content(contentElement); + tooltip.open(); + } } diff --git a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html index eff1ec7bf3..498e1f5eaf 100644 --- a/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html +++ b/ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html @@ -48,6 +48,9 @@ {{ 'tenant.title-required' | translate }} + + {{ 'tenant.title-max-length' | translate }} + { buildEntityForm(entity: TenantInfo): FormGroup { return this.fb.group( { - title: [entity ? entity.title : '', [Validators.required]], + title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]], additionalInfo: this.fb.group( { diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index e4be9ab8a4..f935087bdf 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -21,7 +21,7 @@ - @@ -251,6 +251,7 @@ + - - {{triggerText}} - - +
+
+ +
+
+
+
+ +
+
diff --git a/ui-ngx/src/app/shared/components/help-popup.component.scss b/ui-ngx/src/app/shared/components/help-popup.component.scss index c80b239d2d..7b7de2f022 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.scss +++ b/ui-ngx/src/app/shared/components/help-popup.component.scss @@ -13,18 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +.tb-help-popup-button-container { + width: initial; + display: inline-block; + vertical-align: middle; +} + .tb-help-popup-button { position: relative; -} -.tb-help-popup-button-loading { - background: #fff; - border-radius: 50%; - width: 32px !important; - height: 32px !important; - position: absolute; - top: 0; - left: 0; - &.mat-progress-spinner { + .mat-progress-spinner { + position: absolute; + top: 0; + left: 0; + background: #fff; + border-radius: 50%; + width: 32px !important; + height: 32px !important; svg { top: 6px; left: 6px; @@ -32,36 +37,20 @@ } } -.tb-help-popup-span { +.tb-help-popup-text-button { position: relative; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - color: #2a7dec; - font-weight: 500; - &.active.ready { - text-decoration: underline; + padding: 0 2px 0 8px; + line-height: 28px; + &.mat-stroked-button { + padding: 0 1px 0 7px; + line-height: 26px; } - &:hover { - &:not(.active) { - text-decoration: underline; - } + .mat-icon { + padding-left: 4px; } - &:after { + .mat-progress-spinner { display: inline-block; - font-family: "FontAwesome"; - content: '\f08e'; - padding-left: 5px; - font-size: 14px; - font-weight: 600; - } - .mat-progress-bar { - position: absolute; - height: 2px; - left: 0; - right: 0; - bottom: 0; + margin-left: 4px; + margin-right: 5px; } } diff --git a/ui-ngx/src/app/shared/components/help-popup.component.ts b/ui-ngx/src/app/shared/components/help-popup.component.ts index 0e27a16f5c..26497460fa 100644 --- a/ui-ngx/src/app/shared/components/help-popup.component.ts +++ b/ui-ngx/src/app/shared/components/help-popup.component.ts @@ -17,34 +17,42 @@ import { Component, ElementRef, - Input, + Input, OnChanges, OnDestroy, - Renderer2, + Renderer2, SimpleChanges, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { TbPopoverService } from '@shared/components/popover.service'; import { PopoverPlacement } from '@shared/components/popover.models'; +import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { isDefinedAndNotNull } from '@core/utils'; @Component({ // tslint:disable-next-line:component-selector - selector: '[tb-help-popup]', + selector: '[tb-help-popup], [tb-help-popup-content]', templateUrl: './help-popup.component.html', styleUrls: ['./help-popup.component.scss'], encapsulation: ViewEncapsulation.None }) -export class HelpPopupComponent implements OnDestroy { +export class HelpPopupComponent implements OnChanges, OnDestroy { @ViewChild('toggleHelpButton', {read: ElementRef, static: false}) toggleHelpButton: ElementRef; - @ViewChild('toggleHelpSpan', {read: ElementRef, static: false}) toggleHelpSpan: ElementRef; + @ViewChild('toggleHelpTextButton', {read: ElementRef, static: false}) toggleHelpTextButton: ElementRef; // tslint:disable-next-line:no-input-rename @Input('tb-help-popup') helpId: string; + // tslint:disable-next-line:no-input-rename + @Input('tb-help-popup-content') helpContent: string; + // tslint:disable-next-line:no-input-rename @Input('trigger-text') triggerText: string; + // tslint:disable-next-line:no-input-rename + @Input('trigger-style') triggerStyle: string; + // tslint:disable-next-line:no-input-rename @Input('tb-help-popup-placement') helpPopupPlacement: PopoverPlacement; @@ -54,15 +62,30 @@ export class HelpPopupComponent implements OnDestroy { popoverVisible = false; popoverReady = true; + triggerSafeHtml: SafeHtml = null; + textMode = false; + constructor(private viewContainerRef: ViewContainerRef, + private element: ElementRef, + private sanitizer: DomSanitizer, private renderer: Renderer2, - private popoverService: TbPopoverService) {} + private popoverService: TbPopoverService) { + } + + ngOnChanges(changes: SimpleChanges): void { + if (isDefinedAndNotNull(this.triggerText)) { + this.triggerSafeHtml = this.sanitizer.bypassSecurityTrustHtml(this.triggerText); + } else { + this.triggerSafeHtml = null; + } + this.textMode = this.triggerSafeHtml != null; + } toggleHelp() { - const trigger = this.triggerText ? this.toggleHelpSpan.nativeElement : this.toggleHelpButton.nativeElement; + const trigger = this.textMode ? this.toggleHelpTextButton.nativeElement : this.toggleHelpButton.nativeElement; this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef, this.helpId, - '', + this.helpContent, (visible) => { this.popoverVisible = visible; }, (ready => { diff --git a/ui-ngx/src/app/shared/components/hotkeys.directive.ts b/ui-ngx/src/app/shared/components/hotkeys.directive.ts index 37723a8407..d1419c0ee4 100644 --- a/ui-ngx/src/app/shared/components/hotkeys.directive.ts +++ b/ui-ngx/src/app/shared/components/hotkeys.directive.ts @@ -16,7 +16,8 @@ import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; import { Hotkey } from 'angular2-hotkeys'; -import 'mousetrap'; +import { MousetrapInstance } from 'mousetrap'; +import * as Mousetrap from 'mousetrap'; import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; @Directive({ diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx index db6042f450..12879d1fae 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form-ace-editor.tsx @@ -20,16 +20,15 @@ import Button from '@material-ui/core/Button'; import { JsonFormFieldProps, JsonFormFieldState } from '@shared/components/json-form/react/json-form.models'; import { IEditorProps } from 'react-ace/src/types'; import { mergeMap } from 'rxjs/operators'; -import { loadAceDependencies } from '@shared/models/ace/ace.models'; +import { getAce } from '@shared/models/ace/ace.models'; import { from } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { CircularProgress, IconButton } from '@material-ui/core'; import { MouseEvent } from 'react'; import { Help, HelpOutline } from '@material-ui/icons'; -import { position } from 'html2canvas/dist/types/css/property-descriptors/position'; const ReactAce = React.lazy(() => { - return loadAceDependencies().pipe( + return getAce().pipe( mergeMap(() => { return from(import('react-ace')); }) @@ -205,9 +204,9 @@ class ThingsboardAceEditor extends React.Component Loading...
}> { diff --git a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss index fb33d2afc0..67f737953a 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/json-form.scss +++ b/ui-ngx/src/app/shared/components/json-form/react/json-form.scss @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "~compass-sass-mixins/lib/compass"; $swift-ease-out-duration: .4s !default; $swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; diff --git a/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts b/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts index ef4ba9fc84..1084a7c4c2 100644 --- a/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts +++ b/ui-ngx/src/app/shared/components/json-form/react/styles/thingsboardTheme.ts @@ -31,7 +31,7 @@ */ import indigo from '@material-ui/core/colors/indigo'; import deeepOrange from '@material-ui/core/colors/deepOrange'; -import { ThemeOptions } from '@material-ui/core/styles/createMuiTheme'; +import { ThemeOptions } from '@material-ui/core/styles'; import { PaletteOptions } from '@material-ui/core/styles/createPalette'; import { mergeDeep } from '@core/utils'; diff --git a/ui-ngx/src/app/shared/components/markdown.component.html b/ui-ngx/src/app/shared/components/markdown.component.html new file mode 100644 index 0000000000..b854aa3364 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown.component.html @@ -0,0 +1,26 @@ + + + +
+ {{error}} +
+
+
diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index c9fac8d9a7..1abfc5b9e6 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -18,7 +18,7 @@ import { ChangeDetectorRef, Component, ComponentFactory, - ComponentRef, + ComponentRef, ElementRef, EventEmitter, Inject, Injector, @@ -33,20 +33,17 @@ import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; +import { isDefinedAndNotNull } from '@core/utils'; +import { Observable, of, ReplaySubject } from 'rxjs'; @Component({ selector: 'tb-markdown', - template: '' + - '' + - '
' + - '{{error}}' + - '
' + templateUrl: './markdown.component.html' }) export class TbMarkdownComponent implements OnChanges { @ViewChild('markdownContainer', {read: ViewContainerRef, static: true}) markdownContainer: ViewContainerRef; + @ViewChild('fallbackElement', {static: true}) fallbackElement: ElementRef; @Input() data: string | undefined; @@ -58,9 +55,14 @@ export class TbMarkdownComponent implements OnChanges { get lineNumbers(): boolean { return this.lineNumbersValue; } set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } + @Input() + get fallbackToPlainMarkdown(): boolean { return this.fallbackToPlainMarkdownValue; } + set fallbackToPlainMarkdown(value: boolean) { this.fallbackToPlainMarkdownValue = coerceBooleanProperty(value); } + @Output() ready = new EventEmitter(); private lineNumbersValue = false; + private fallbackToPlainMarkdownValue = false; isMarkdownReady = false; @@ -76,14 +78,14 @@ export class TbMarkdownComponent implements OnChanges { private dynamicComponentFactoryService: DynamicComponentFactoryService) {} ngOnChanges(changes: SimpleChanges): void { - if (this.data) { + if (isDefinedAndNotNull(this.data)) { this.render(this.data); } } private render(markdown: string) { - let template = this.markdownService.compile(markdown, false); - template = this.sanitizeCurlyBraces(template); + const compiled = this.markdownService.compile(markdown, false); + let template = this.sanitizeCurlyBraces(compiled); let markdownClass = 'tb-markdown-view'; if (this.markdownClass) { markdownClass += ` ${this.markdownClass}`; @@ -91,6 +93,7 @@ export class TbMarkdownComponent implements OnChanges { template = `
${template}
`; this.markdownContainer.clear(); const parent = this; + let readyObservable: Observable; this.dynamicComponentFactoryService.createDynamicComponentFactory( class TbMarkdownInstance { ngOnDestroy(): void { @@ -109,22 +112,40 @@ export class TbMarkdownComponent implements OnChanges { this.tbMarkdownInstanceComponentRef.instance.style = this.style; this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement); this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement); + readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); this.error = null; } catch (error) { - this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); - this.destroyMarkdownInstanceResources(); + readyObservable = this.handleError(compiled, error); } this.cd.detectChanges(); - this.ready.emit(); + readyObservable.subscribe(() => { + this.ready.emit(); + }); }, (error) => { - this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); - this.destroyMarkdownInstanceResources(); + readyObservable = this.handleError(compiled, error); this.cd.detectChanges(); - this.ready.emit(); + readyObservable.subscribe(() => { + this.ready.emit(); + }); }); } + private handleError(template: string, error): Observable { + this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); + this.destroyMarkdownInstanceResources(); + if (this.fallbackToPlainMarkdownValue) { + this.markdownContainer.clear(); + const element = this.fallbackElement.nativeElement; + element.innerHTML = template; + this.handlePlugins(element); + this.markdownService.highlight(element); + return this.handleImages(element); + } else { + return of(null); + } + } + private handlePlugins(element: HTMLElement): void { if (this.lineNumbers) { this.setPluginClass(element, PrismPlugin.LineNumbers); @@ -139,6 +160,26 @@ export class TbMarkdownComponent implements OnChanges { } } + private handleImages(element: HTMLElement): Observable { + const imgs = $('img', element); + if (imgs.length) { + let totalImages = imgs.length; + const imagesLoadedSubject = new ReplaySubject(); + imgs.each((index, img) => { + $(img).one('load error', () => { + totalImages--; + if (totalImages === 0) { + imagesLoadedSubject.next(); + imagesLoadedSubject.complete(); + } + }); + }); + return imagesLoadedSubject.asObservable(); + } else { + return of(null); + } + } + private sanitizeCurlyBraces(template: string): string { return template.replace(/{/g, '{').replace(/}/g, '}'); } diff --git a/ui-ngx/src/app/shared/components/marked-options.service.ts b/ui-ngx/src/app/shared/components/marked-options.service.ts index 89ed434e70..729d6d5d5b 100644 --- a/ui-ngx/src/app/shared/components/marked-options.service.ts +++ b/ui-ngx/src/app/shared/components/marked-options.service.ts @@ -19,8 +19,13 @@ import { Inject, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { DOCUMENT } from '@angular/common'; import { WINDOW } from '@core/services/window.service'; +import { Tokenizer } from 'marked'; +import * as marked from 'marked'; +import { Clipboard } from '@angular/cdk/clipboard'; const copyCodeBlock = '{:copy-code}'; +const codeStyleRegex = '^{:code-style="(.*)"}\n'; +const autoBlock = '{:auto}'; const targetBlankBlock = '{:target="_blank"}'; // @dynamic @@ -36,33 +41,68 @@ export class MarkedOptionsService extends MarkedOptions { pedantic = false; smartLists = true; smartypants = false; + mangle = false; private renderer2 = new MarkedRenderer(); private id = 1; constructor(private translate: TranslateService, + private clipboardService: Clipboard, @Inject(WINDOW) private readonly window: Window, @Inject(DOCUMENT) private readonly document: Document) { super(); + // @ts-ignore + const tokenizer: Tokenizer = { + autolink(src: string, mangle: (cap: string) => string): marked.Tokens.Link { + if (src.endsWith(copyCodeBlock)) { + return undefined; + } else { + // @ts-ignore + return false; + } + }, + url(src: string, mangle: (cap: string) => string): marked.Tokens.Link { + if (src.endsWith(copyCodeBlock)) { + return undefined; + } else { + // @ts-ignore + return false; + } + } + }; + marked.use({tokenizer}); this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => { - if (code.endsWith(copyCodeBlock)) { - code = code.substring(0, code.length - copyCodeBlock.length); - const content = checkLineNumbers(this.renderer2.code(code, language, isEscaped), code); + const codeContext = processCode(code); + if (codeContext.copyCode) { + const content = postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext); this.id++; - return this.wrapCopyCode(this.id, content, code); + return this.wrapCopyCode(this.id, content, codeContext); } else { - return this.wrapDiv(checkLineNumbers(this.renderer2.code(code, language, isEscaped), code)); + return this.wrapDiv(postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext)); } }; + this.renderer.table = (header: string, body: string) => { + let autoLayout = false; + if (header.includes(autoBlock)) { + autoLayout = true; + header = header.replace(autoBlock, ''); + } + let table = this.renderer2.table(header, body); + if (autoLayout) { + table = table.replace(' { - if (content.endsWith(copyCodeBlock)) { - content = content.substring(0, content.length - copyCodeBlock.length); + const codeContext = processCode(content); + codeContext.multiline = false; + if (codeContext.copyCode) { this.id++; - content = this.wrapCopyCode(this.id, content, content); + content = this.wrapCopyCode(this.id, codeContext.code, codeContext); } return this.renderer2.tablecell(content, flags); }; @@ -83,10 +123,14 @@ export class MarkedOptionsService extends MarkedOptions { return `
${content}
`; } - private wrapCopyCode(id: number, content: string, code: string): string { + private wrapCopyCode(id: number, content: string, context: CodeContext): string { + let copyCodeButtonClass = 'clipboard-btn'; + if (context.multiline) { + copyCodeButtonClass += ' multiline'; + } return `
${content}` + - `` + - ` +
+
+ +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/shared/components/protobuf-content.component.scss b/ui-ngx/src/app/shared/components/protobuf-content.component.scss new file mode 100644 index 0000000000..369d172491 --- /dev/null +++ b/ui-ngx/src/app/shared/components/protobuf-content.component.scss @@ -0,0 +1,57 @@ +/** + * 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. + */ +:host { + position: relative; + + .fill-height { + height: 100%; + } +} + +.tb-protobuf-content-toolbar { + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { + align-items: center; + vertical-align: middle; + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + &:not(:last-child) { + margin-right: 4px; + } + } +} + +.tb-protobuf-content-panel { + height: 100%; + margin-left: 15px; + border: 1px solid #c0c0c0; + + #tb-protobuf-input { + width: 100%; + min-width: 200px; + min-height: 160px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/protobuf-content.component.ts b/ui-ngx/src/app/shared/components/protobuf-content.component.ts new file mode 100644 index 0000000000..0c1e7c7fac --- /dev/null +++ b/ui-ngx/src/app/shared/components/protobuf-content.component.ts @@ -0,0 +1,193 @@ +/// +/// 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 { + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { guid } from '@core/utils'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { getAce } from '@shared/models/ace/ace.models'; +import { beautifyJs } from '@shared/models/beautify.models'; + +@Component({ + selector: 'tb-protobuf-content', + templateUrl: './protobuf-content.component.html', + styleUrls: ['./protobuf-content.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ProtobufContentComponent), + multi: true + } + ] +}) +export class ProtobufContentComponent implements OnInit, ControlValueAccessor, OnDestroy { + + @ViewChild('protobufEditor', {static: true}) + protobufEditorElmRef: ElementRef; + + private protobufEditor: Ace.Editor; + private editorsResizeCaf: CancelAnimationFrame; + private editorResize$: ResizeObserver; + private ignoreChange = false; + + toastTargetId = `protobufContentEditor-${guid()}`; + + @Input() label: string; + + @Input() disabled: boolean; + + @Input() fillHeight: boolean; + + @Input() editorStyle: {[klass: string]: any}; + + @Input() tbPlaceholder: string; + + private readonlyValue: boolean; + get readonly(): boolean { + return this.readonlyValue; + } + @Input() + set readonly(value: boolean) { + this.readonlyValue = coerceBooleanProperty(value); + } + + fullscreen = false; + + contentBody: string; + + errorShowed = false; + + private propagateChange = null; + + constructor(public elementRef: ElementRef, + protected store: Store, + private raf: RafService) { + } + + ngOnInit(): void { + const editorElement = this.protobufEditorElmRef.nativeElement; + let editorOptions: Partial = { + mode: `ace/mode/protobuf`, + showGutter: true, + showPrintMargin: false, + readOnly: this.disabled || this.readonly, + }; + + const advancedOptions = { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + this.protobufEditor = ace.edit(editorElement, editorOptions); + this.protobufEditor.session.setUseWrapMode(true); + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1); + this.protobufEditor.setReadOnly(this.disabled || this.readonly); + this.protobufEditor.on('change', () => { + if (!this.ignoreChange) { + this.updateView(); + } + }); + this.editorResize$ = new ResizeObserver(() => { + this.onAceEditorResize(); + }); + this.editorResize$.observe(editorElement); + } + ); + } + + ngOnDestroy(): void { + if (this.editorResize$) { + this.editorResize$.disconnect(); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.protobufEditor) { + this.protobufEditor.setReadOnly(this.disabled || this.readonly); + } + } + + writeValue(value: string): void { + this.contentBody = value; + if (this.protobufEditor) { + this.ignoreChange = true; + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1); + this.ignoreChange = false; + } + } + + updateView() { + const editorValue = this.protobufEditor.getValue(); + if (this.contentBody !== editorValue) { + this.contentBody = editorValue; + this.propagateChange(this.contentBody); + } + } + + beautifyProtobuf() { + beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe( + (res) => { + this.protobufEditor.setValue(res ? res : '', -1); + this.updateView(); + } + ); + } + + onFullscreen() { + if (this.protobufEditor) { + setTimeout(() => { + this.protobufEditor.resize(); + }, 0); + } + } + + private onAceEditorResize() { + if (this.editorsResizeCaf) { + this.editorsResizeCaf(); + this.editorsResizeCaf = null; + } + this.editorsResizeCaf = this.raf.raf(() => { + this.protobufEditor.resize(); + this.protobufEditor.renderer.updateFull(); + }); + } + +} diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html index cd60473b00..b572f6b9df 100644 --- a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.html @@ -39,4 +39,7 @@ {{ 'relation.relation-type-required' | translate }} + + {{ 'relation.relation-type-max-length' | translate }} + diff --git a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts index d425e90b64..4477b9b2f0 100644 --- a/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/relation/relation-type-autocomplete.component.ts @@ -68,7 +68,7 @@ export class RelationTypeAutocompleteComponent implements ControlValueAccessor, public translate: TranslateService, private fb: FormBuilder) { this.relationTypeFormGroup = this.fb.group({ - relationType: [null, this.required ? [Validators.required] : []] + relationType: [null, this.required ? [Validators.required, Validators.maxLength(255)] : [Validators.maxLength(255)]] }); } diff --git a/ui-ngx/src/app/shared/components/tokens.ts b/ui-ngx/src/app/shared/components/tokens.ts index 8171d52839..205357b6f9 100644 --- a/ui-ngx/src/app/shared/components/tokens.ts +++ b/ui-ngx/src/app/shared/components/tokens.ts @@ -21,4 +21,4 @@ export const HELP_MARKDOWN_COMPONENT_TOKEN: InjectionToken> = new InjectionToken>('HELP_MARKDOWN_COMPONENT_TOKEN'); export const SHARED_MODULE_TOKEN: InjectionToken> = - new InjectionToken>('HELP_MARKDOWN_COMPONENT_TOKEN'); + new InjectionToken>('SHARED_MODULE_TOKEN'); diff --git a/ui-ngx/src/app/shared/models/ace/ace.models.ts b/ui-ngx/src/app/shared/models/ace/ace.models.ts index c352923761..3b088e01d3 100644 --- a/ui-ngx/src/app/shared/models/ace/ace.models.ts +++ b/ui-ngx/src/app/shared/models/ace/ace.models.ts @@ -22,7 +22,7 @@ import { map, mergeMap, tap } from 'rxjs/operators'; let aceDependenciesLoaded = false; let aceModule: any; -export function loadAceDependencies(): Observable { +function loadAceDependencies(): Observable { if (aceDependenciesLoaded) { return of(null); } else { @@ -36,6 +36,8 @@ export function loadAceDependencies(): Observable { aceObservables.push(from(import('ace-builds/src-noconflict/mode-text'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-html'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp'))); + aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json'))); @@ -43,6 +45,9 @@ export function loadAceDependencies(): Observable { aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp'))); + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf'))); + aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate'))); aceObservables.push(from(import('ace-builds/src-noconflict/theme-github'))); return forkJoin(aceObservables).pipe( tap(() => { diff --git a/ui-ngx/src/app/shared/models/component-descriptor.models.ts b/ui-ngx/src/app/shared/models/component-descriptor.models.ts index e266113848..068a902b18 100644 --- a/ui-ngx/src/app/shared/models/component-descriptor.models.ts +++ b/ui-ngx/src/app/shared/models/component-descriptor.models.ts @@ -21,7 +21,8 @@ export enum ComponentType { FILTER = 'FILTER', TRANSFORMATION = 'TRANSFORMATION', ACTION = 'ACTION', - EXTERNAL = 'EXTERNAL' + EXTERNAL = 'EXTERNAL', + FLOW = 'FLOW' } export enum ComponentScope { diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 03b38345ca..9bf120f8b7 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -15,6 +15,7 @@ /// import { InjectionToken } from '@angular/core'; +import { IModulesMap } from '@modules/common/modules-map.models'; export const Constants = { serverErrorCode: { @@ -99,7 +100,8 @@ export const HelpLinks = { ruleNodeSaveAttributes: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node', ruleNodeSaveTimeseries: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node', ruleNodeSaveToCustomTable: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table', - ruleNodeRuleChain: helpBaseUrl + '/docs/user-guide/ui/rule-chains/', + ruleNodeRuleChain: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/flow-nodes/#rule-chain-node', + ruleNodeOutputNode: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/flow-nodes/#output-node', ruleNodeAwsSns: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node', ruleNodeAwsSqs: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node', ruleNodeKafka: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/external-nodes/#kafka-node', @@ -244,4 +246,4 @@ export const contentTypesMap = new Map( export const customTranslationsPrefix = 'custom.'; export const i18nPrefix = 'i18n'; -export const MODULES_MAP = new InjectionToken<{[key: string]: any}>('ModulesMap'); +export const MODULES_MAP = new InjectionToken('ModulesMap'); diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index d27036d7df..7ebf7bde47 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -247,6 +247,8 @@ export interface MqttDeviceProfileTransportConfiguration { deviceAttributesTopic?: string; transportPayloadTypeConfiguration?: { transportPayloadType?: TransportPayloadType; + enableCompatibilityWithJsonPayloadFormat?: boolean; + useJsonPayloadFormatForDefaultDownlinkTopics?: boolean; }; [key: string]: any; } @@ -359,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT const mqttTransportConfiguration: MqttDeviceProfileTransportConfiguration = { deviceTelemetryTopic: 'v1/devices/me/telemetry', deviceAttributesTopic: 'v1/devices/me/attributes', - transportPayloadTypeConfiguration: {transportPayloadType: TransportPayloadType.JSON} + transportPayloadTypeConfiguration: { + transportPayloadType: TransportPayloadType.JSON, + enableCompatibilityWithJsonPayloadFormat: false, + useJsonPayloadFormatForDefaultDownlinkTopics: false, + } }; transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; break; diff --git a/ui-ngx/src/app/shared/models/event.models.ts b/ui-ngx/src/app/shared/models/event.models.ts index ac7d87c0c4..4970c3b83e 100644 --- a/ui-ngx/src/app/shared/models/event.models.ts +++ b/ui-ngx/src/app/shared/models/event.models.ts @@ -91,13 +91,13 @@ export interface BaseFilterEventBody { export interface ErrorFilterEventBody extends BaseFilterEventBody { method?: string; - error?: string; + errorStr?: string; } export interface LcFilterEventEventBody extends BaseFilterEventBody { event?: string; status?: string; - error?: string; + errorStr?: string; } export interface StatsFilterEventBody extends BaseFilterEventBody { @@ -115,7 +115,7 @@ export interface DebugFilterRuleNodeEventBody extends BaseFilterEventBody { dataSearch?: string; metadataSearch?: string; isError?: boolean; - error?: string; + errorStr?: string; } export type FilterEventBody = ErrorFilterEventBody & LcFilterEventEventBody & StatsFilterEventBody & DebugFilterRuleNodeEventBody; diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts index 925ad0b432..61f70bfcb6 100644 --- a/ui-ngx/src/app/shared/models/rule-chain.models.ts +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -38,17 +38,11 @@ export interface RuleChainMetaData { firstNodeIndex?: number; nodes: Array; connections: Array; - ruleChainConnections: Array; -} - -export interface ResolvedRuleChainMetaData extends RuleChainMetaData { - targetRuleChainsMap: {[ruleChainId: string]: RuleChain}; } export interface RuleChainImport { ruleChain: RuleChain; metadata: RuleChainMetaData; - resolvedMetadata?: ResolvedRuleChainMetaData; } export interface NodeConnectionInfo { @@ -57,39 +51,16 @@ export interface NodeConnectionInfo { type: string; } -export interface RuleChainConnectionInfo { - fromIndex: number; - targetRuleChainId: RuleChainId; - additionalInfo: any; - type: string; -} - export const ruleNodeTypeComponentTypes: ComponentType[] = [ ComponentType.FILTER, ComponentType.ENRICHMENT, ComponentType.TRANSFORMATION, ComponentType.ACTION, - ComponentType.EXTERNAL + ComponentType.EXTERNAL, + ComponentType.FLOW ]; -export const ruleChainNodeComponent: RuleNodeComponentDescriptor = { - type: RuleNodeType.RULE_CHAIN, - name: 'rule chain', - clazz: 'tb.internal.RuleChain', - configurationDescriptor: { - nodeDefinition: { - description: '', - details: 'Forwards incoming messages to specified Rule Chain', - inEnabled: true, - outEnabled: false, - relationTypes: [], - customRelations: false, - defaultConfiguration: {} - } - } -}; - export const unknownNodeComponent: RuleNodeComponentDescriptor = { type: RuleNodeType.UNKNOWN, name: 'unknown', diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 489c6eef10..f30574ce7b 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -18,7 +18,7 @@ import { BaseData } from '@shared/models/base-data'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { RuleNodeId } from '@shared/models/id/rule-node-id'; import { ComponentDescriptor } from '@shared/models/component-descriptor.models'; -import { FcEdge, FcNode } from 'ngx-flowchart/dist/ngx-flowchart'; +import { FcEdge, FcNode } from 'ngx-flowchart'; import { Observable } from 'rxjs'; import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, EventEmitter, Inject, OnInit, Directive } from '@angular/core'; @@ -52,6 +52,7 @@ export interface RuleNodeDefinition { outEnabled: boolean; relationTypes: string[]; customRelations: boolean; + ruleChainNode?: boolean; defaultConfiguration: RuleNodeConfiguration; icon?: string; iconUrl?: string; @@ -67,6 +68,8 @@ export interface RuleNodeConfigurationDescriptor { export interface IRuleNodeConfigurationComponent { ruleNodeId: string; + ruleChainId: string; + ruleChainType: RuleChainType; configuration: RuleNodeConfiguration; configurationChanged: Observable; validate(); @@ -74,11 +77,16 @@ export interface IRuleNodeConfigurationComponent { } @Directive() +// tslint:disable-next-line:directive-class-suffix export abstract class RuleNodeConfigurationComponent extends PageComponent implements IRuleNodeConfigurationComponent, OnInit, AfterViewInit { ruleNodeId: string; + ruleChainId: string; + + ruleChainType: RuleChainType; + configurationValue: RuleNodeConfiguration; private configurationSet = false; @@ -184,7 +192,7 @@ export enum RuleNodeType { TRANSFORMATION = 'TRANSFORMATION', ACTION = 'ACTION', EXTERNAL = 'EXTERNAL', - RULE_CHAIN = 'RULE_CHAIN', + FLOW = 'FLOW', UNKNOWN = 'UNKNOWN', INPUT = 'INPUT' } @@ -195,7 +203,7 @@ export const ruleNodeTypesLibrary = [ RuleNodeType.TRANSFORMATION, RuleNodeType.ACTION, RuleNodeType.EXTERNAL, - RuleNodeType.RULE_CHAIN, + RuleNodeType.FLOW, ]; export interface RuleNodeTypeDescriptor { @@ -260,12 +268,12 @@ export const ruleNodeTypeDescriptors = new Map( ] ); +export const ruleChainNodeClazz = 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode'; +export const outputNodeClazz = 'org.thingsboard.rule.engine.flow.TbRuleChainOutputNode'; + const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.filter.TbCheckRelationNode': 'ruleNodeCheckRelation', 'org.thingsboard.rule.engine.filter.TbCheckMessageNode': 'ruleNodeCheckExistenceFields', @@ -424,7 +434,6 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode': 'ruleNodeSaveAttributes', 'org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode': 'ruleNodeSaveTimeseries', 'org.thingsboard.rule.engine.action.TbSaveToCustomCassandraTableNode': 'ruleNodeSaveToCustomTable', - 'tb.internal.RuleChain': 'ruleNodeRuleChain', 'org.thingsboard.rule.engine.aws.sns.TbSnsNode': 'ruleNodeAwsSns', 'org.thingsboard.rule.engine.aws.sqs.TbSqsNode': 'ruleNodeAwsSqs', 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka', @@ -435,7 +444,9 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail', 'org.thingsboard.rule.engine.sms.TbSendSmsNode': 'ruleNodeSendSms', 'org.thingsboard.rule.engine.edge.TbMsgPushToCloudNode': 'ruleNodePushToCloud', - 'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge' + 'org.thingsboard.rule.engine.edge.TbMsgPushToEdgeNode': 'ruleNodePushToEdge', + 'org.thingsboard.rule.engine.flow.TbRuleChainInputNode': 'ruleNodeRuleChain', + 'org.thingsboard.rule.engine.flow.TbRuleChainOutputNode': 'ruleNodeOutputNode', }; export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): string { diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index bf929637c8..0b0353c7a9 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -152,6 +152,7 @@ export interface WidgetTypeParameters { useCustomDatasources?: boolean; maxDatasources?: number; maxDataKeys?: number; + datasourcesOptional?: boolean; dataKeysOptional?: boolean; stateData?: boolean; hasDataPageLink?: boolean; @@ -508,6 +509,7 @@ export interface WidgetConfig { titleStyle?: {[klass: string]: any}; units?: string; decimals?: number; + noDataDisplayMessage?: string; actions?: {[actionSourceId: string]: Array}; settings?: any; alarmSource?: Datasource; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 2b1de653ec..1f66aedf47 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -21,7 +21,7 @@ import { LogoComponent } from '@shared/components/logo.component'; import { TbSnackBarComponent, ToastDirective } from '@shared/components/toast.directive'; import { BreadcrumbComponent } from '@shared/components/breadcrumb.component'; import { FlowInjectionToken, NgxFlowModule } from '@flowjs/ngx-flow'; -import { NgxFlowchartModule } from 'ngx-flowchart/dist/ngx-flowchart'; +import { NgxFlowchartModule } from 'ngx-flowchart'; import Flow from '@flowjs/flow.js'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; @@ -132,7 +132,7 @@ import { NavTreeComponent } from '@shared/components/nav-tree.component'; import { LedLightComponent } from '@shared/components/led-light.component'; import { TbJsonToStringDirective } from '@shared/components/directives/tb-json-to-string.directive'; import { JsonObjectEditDialogComponent } from '@shared/components/dialog/json-object-edit-dialog.component'; -import { HistorySelectorComponent } from './components/time/history-selector/history-selector.component'; +import { HistorySelectorComponent } from '@shared/components/time/history-selector/history-selector.component'; import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-gateway-select.component'; import { DndModule } from 'ngx-drag-drop'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; @@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service' import { TbPopoverService } from '@shared/components/popover.service'; import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { TbMarkdownComponent } from '@shared/components/markdown.component'; +import { ProtobufContentComponent } from '@shared/components/protobuf-content.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) OtaPackageAutocompleteComponent, WidgetsBundleSearchComponent, CopyButtonComponent, - TogglePasswordComponent + TogglePasswordComponent, + ProtobufContentComponent ], imports: [ CommonModule, @@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) OtaPackageAutocompleteComponent, WidgetsBundleSearchComponent, CopyButtonComponent, - TogglePasswordComponent + TogglePasswordComponent, + ProtobufContentComponent ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/add_location.svg b/ui-ngx/src/assets/add_location.svg deleted file mode 100644 index e3150115b7..0000000000 --- a/ui-ngx/src/assets/add_location.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/ui-ngx/src/assets/add_polygon.svg b/ui-ngx/src/assets/add_polygon.svg deleted file mode 100644 index 098f02c128..0000000000 --- a/ui-ngx/src/assets/add_polygon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/ui-ngx/src/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md new file mode 100644 index 0000000000..74e4c83828 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md @@ -0,0 +1,68 @@ +#### Clear alarm details builder function + +
+
+ +*function Details(msg, metadata, msgType): any* + +JavaScript function generating **Alarm Details** object to update existing one. Used for storing additional parameters inside Alarm.
+For example you can save attribute name/value pair from Original Message payload or Metadata. + +**Parameters:** + +{% include rulenode/common_node_script_args %} + +**Returns:** + +Should return the object presenting **Alarm Details**. + +Current Alarm Details can be accessed via `metadata.prevAlarmDetails`.
+**Note** that `metadata.prevAlarmDetails` is a raw String field, and it needs to be converted into object using this construction: + +```javascript +var details = {}; +if (metadata.prevAlarmDetails) { + // remove prevAlarmDetails from metadata + delete metadata.prevAlarmDetails; + details = JSON.parse(metadata.prevAlarmDetails); +} +{:copy-code} +``` + +
+ +##### Examples + +
    +
  • +Take count property from previous Alarm and increment it.
    +Also put temperature attribute from inbound Message payload into Alarm details: +
  • +
+ +```javascript +var details = {temperature: msg.temperature, count: 1}; + +if (metadata.prevAlarmDetails) { + var prevDetails = JSON.parse(metadata.prevAlarmDetails); + // remove prevAlarmDetails from metadata + delete metadata.prevAlarmDetails; + if (prevDetails.count) { + details.count = prevDetails.count + 1; + } +} + +return details; +{:copy-code} +``` + +
+ +More details about Alarms can be found in [this tutorial{:target="_blank"}](${siteBaseUrl}/docs/user-guide/alarms/). + +You can see the real life example, where this node is used, in the next tutorial: + +- [Create and Clear Alarms{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/) + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md new file mode 100644 index 0000000000..296e5b735c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md @@ -0,0 +1,11 @@ +
    +
  • msg: {[key: string]: any} - is a Message payload key/value object. +
  • +
  • metadata: {[key: string]: string} - is a Message metadata key/value map, where both keys and values are strings. +
  • +
  • msgType: string - is a string containing Message type. See MessageType enum for common used values. +
  • +
+ +Enable 'debug mode' for your rule node to see the messages that arrive in near real-time. +See Debugging for more information. \ No newline at end of file diff --git a/ui-ngx/src/assets/help/en_US/rulenode/create_alarm_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/create_alarm_node_script_fn.md new file mode 100644 index 0000000000..c6c2fe2481 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/create_alarm_node_script_fn.md @@ -0,0 +1,69 @@ +#### Create alarm details builder function + +
+
+ +*function Details(msg, metadata, msgType): any* + +JavaScript function generating **Alarm Details** object. Used for storing additional parameters inside Alarm.
+For example you can save attribute name/value pair from Original Message payload or Metadata. + +**Parameters:** + +{% include rulenode/common_node_script_args %} + +**Returns:** + +Should return the object presenting **Alarm Details**. + +**Optional:** previous Alarm Details can be accessed via `metadata.prevAlarmDetails`.
+If previous Alarm does not exist, this field will not be present in Metadata. **Note** that `metadata.prevAlarmDetails`
+is a raw String field, and it needs to be converted into object using this construction: + +```javascript +var details = {}; +if (metadata.prevAlarmDetails) { + // remove prevAlarmDetails from metadata + delete metadata.prevAlarmDetails; + details = JSON.parse(metadata.prevAlarmDetails); +} +{:copy-code} +``` + +
+ +##### Examples + +
    +
  • +Take count property from previous Alarm and increment it.
    +Also put temperature attribute from inbound Message payload into Alarm details: +
  • +
+ +```javascript +var details = {temperature: msg.temperature, count: 1}; + +if (metadata.prevAlarmDetails) { + var prevDetails = JSON.parse(metadata.prevAlarmDetails); + // remove prevAlarmDetails from metadata + delete metadata.prevAlarmDetails; + if (prevDetails.count) { + details.count = prevDetails.count + 1; + } +} + +return details; +{:copy-code} +``` + +
+ +More details about Alarms can be found in [this tutorial{:target="_blank"}](${siteBaseUrl}/docs/user-guide/alarms/). + +You can see the real life example, where this node is used, in the next tutorial: + +- [Create and Clear Alarms{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/) + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/filter_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/filter_node_script_fn.md new file mode 100644 index 0000000000..862590867b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/filter_node_script_fn.md @@ -0,0 +1,84 @@ +#### Filter message function + +
+
+ +*function Filter(msg, metadata, msgType): boolean* + +JavaScript function defines a boolean expression based on the incoming Message and Metadata. + +**Parameters:** + +{% include rulenode/common_node_script_args %} + +**Returns:** + +Must return a `boolean` value. If `true` - routes Message to subsequent rule nodes that are related via **True** link, +otherwise sends Message to rule nodes related via **False** link. +Uses 'Failure' link in case of any failures to evaluate the expression. + +
+ +##### Examples + +* Forward all messages with `temperature` value greater than `20` to the **True** link and all other messages to the **False** link. + Assumes that incoming messages always contain the 'temperature' field: + +```javascript +return msg.temperature > 20; +{:copy-code} +``` + + +Example of the rule chain configuration: + +![image](${helpBaseUrl}/help/images/rulenode/examples/filter-node.png) + +* Same as above, but checks that the message has 'temperature' field to **avoid failures** on unexpected messages: + +```javascript +return typeof msg.temperature !== 'undefined' && msg.temperature > 20; +{:copy-code} +``` + +* Forward all messages with type `ATTRIBUTES_UPDATED` to the **True** chain and all other messages to the **False** chain: + +```javascript +if (msgType === 'ATTRIBUTES_UPDATED') { + return true; +} else { + return false; +} +{:copy-code} +``` + +
    +
  • Send message to the True chain if the following conditions are met.
    Message type is POST_TELEMETRY_REQUEST and
    +(device type is vehicle and humidity value is greater than 50 or
    +device type is controller and temperature value is greater than 20 and humidity value is greater than 60).
    +Otherwise send message to the False chain: +
  • +
+ +```javascript +if (msgType === 'POST_TELEMETRY_REQUEST') { + if (metadata.deviceType === 'vehicle') { + return msg.humidity > 50; + } else if (metadata.deviceType === 'controller') { + return msg.temperature > 20 && msg.humidity > 60; + } +} +return false; +{:copy-code} +``` + +
+ +You can see real life example, how to use this node in those tutorials: + +- [Create and Clear Alarms{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/#node-a-filter-script) +- [Reply to RPC Calls{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-filter-script-node) + +
+
+ diff --git a/ui-ngx/src/assets/help/en_US/rulenode/generator_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/generator_node_script_fn.md new file mode 100644 index 0000000000..ac8b6abf56 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/generator_node_script_fn.md @@ -0,0 +1,112 @@ +#### Message generator function + +
+
+ +*function Generate(prevMsg, prevMetadata, prevMsgType): {msg: object, metadata: object, msgType: string}* + +JavaScript function generating new Message using previous Message payload, Metadata and Message type as input arguments. + +**Parameters:** + +
    +
  • prevMsg: {[key: string]: any} - is a previously generated Message payload key/value object. +
  • +
  • prevMetadata: {[key: string]: string} - is a previously generated Message metadata key/value object. +
  • +
  • prevMsgType: string - is a previously generated string Message type. See MessageType enum for common used values. +
  • +
+ +**Returns:** + +Should return the object with the following structure: + +```javascript +{ + msg: {[key: string]: any}, + metadata: {[key: string]: string}, + msgType: string +} +``` + +All fields in resulting object are mandatory. + +
+ +##### Examples + +* Generate message of type `POST_TELEMETRY_REQUEST` with random `temperature` value from `18` to `32`: + +```javascript +var temperature = 18 + Math.random() * (32 - 18); +// Round to at most 2 decimal places (optional) +temperature = Math.round( temperature * 100 ) / 100; +var msg = { temperature: temperature }; +return { msg: msg, metadata: {}, msgType: "POST_TELEMETRY_REQUEST" }; +{:copy-code} +``` + + +
    +
  • +Generate message of type POST_TELEMETRY_REQUEST with temp value 42, +humidity value 77
    +and metadata with field data having value 40: +
  • +
+ +```javascript +var msg = { temp: 42, humidity: 77 }; +var metadata = { data: 40 }; +return { msg: msg, metadata: metadata, msgType: "POST_TELEMETRY_REQUEST" }; +{:copy-code} +``` + +
    +
  • +Generate message of type POST_TELEMETRY_REQUEST with temperature value
    +increasing and decreasing linearly in the range from 18 to 32: +
  • +
+ +```javascript +var lower = 18; +var upper = 32; +var isDecrement = 'false'; +var temperature = lower; + +// Get previous values + +if (typeof prevMetadata !== 'undefined' && + typeof prevMetadata.isDecrement !== 'undefined') { + isDecrement = prevMetadata.isDecrement; +} +if (typeof prevMsg !== 'undefined' && + typeof prevMsg.temperature !== 'undefined') { + temperature = prevMsg.temperature; +} + +if (isDecrement === 'true') { + temperature--; + if (temperature <= lower) { + isDecrement = 'false'; + temperature = lower; + } +} else { + temperature++; + if (temperature >= upper) { + isDecrement = 'true'; + temperature = upper; + } +} + +var msg = { temperature: temperature }; +var metadata = { isDecrement: isDecrement }; + +return { msg: msg, metadata: metadata, msgType: "POST_TELEMETRY_REQUEST" }; +{:copy-code} +``` + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/log_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/log_node_script_fn.md new file mode 100644 index 0000000000..ababc52ffe --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/log_node_script_fn.md @@ -0,0 +1,37 @@ +#### Message to string function + +
+
+ +*function toString(msg, metadata, msgType): string* + +JavaScript function transforming incoming Message to String for further logging to the server log file. + +**Parameters:** + +{% include rulenode/common_node_script_args %} + +**Returns:** + +Should return `string` value used for logging to the server log file. + +
+ +##### Examples + +* Create string message containing incoming message and incoming metadata values: + +```javascript +return 'Incoming message:\n' + JSON.stringify(msg) + + '\nIncoming metadata:\n' + JSON.stringify(metadata); +{:copy-code} +``` + +
+ +You can see real life example, how to use this node in this tutorial: + +- [Reply to RPC Calls{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#log-unknown-request) + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md new file mode 100644 index 0000000000..a31eae72cf --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md @@ -0,0 +1,101 @@ +#### Switch message function + +
+
+ +*function Switch(msg, metadata, msgType): string[]* + +JavaScript function computing **an array of Link names** to forward the incoming Message. + +**Parameters:** + +{% include rulenode/common_node_script_args %} + +**Returns:** + +Should return an array of `string` values presenting **link names** that the Rule Engine should use to further route the incoming Message.
+If the result is an empty array - message will not be routed to any Node and will be immediately +acknowledged. + +
+ +##### Examples + +
    +
  • +Forward all messages with temperature value greater than 30 to the 'High temperature' chain,
    +with temperature value lower than 20 to the 'Low temperature' chain and all other messages
    +to the 'Other' chain: +
  • +
+ +```javascript +if (msg.temperature > 30) { + return ['High temperature']; +} else if (msg.temperature < 20) { + return ['Low temperature']; +} else { + return ['Other']; +} +{:copy-code} +``` + +Example of the rule chain configuration: + +![image](${helpBaseUrl}/help/images/rulenode/examples/switch-node.png) + +
    +
  • + For messages with type POST_TELEMETRY_REQUEST: +
      +
    • + if temperature value lower than 18 forward to the 'Low temperature telemetry' chain, +
    • +
    • + otherwise to the 'Normal temperature telemetry' chain. +
    • +
    + For messages with type POST_ATTRIBUTES_REQUEST:
    +
      +
    • + if currentState value is IDLE forward to the 'Idle State' and 'Update State Attribute' chains, +
    • +
    • + if currentState value is RUNNING forward to the 'Running State' and 'Update State Attribute' chains, +
    • +
    • + otherwise to the 'Unknown State' chain. +
    • +
    + For all other message types - discard the message (do not route to any Node). +
  • +
+ +```javascript +if (msgType === 'POST_TELEMETRY_REQUEST') { + if (msg.temperature < 18) { + return ['Low Temperature Telemetry']; + } else { + return ['Normal Temperature Telemetry']; + } +} else if (msgType === 'POST_ATTRIBUTES_REQUEST') { + if (msg.currentState === 'IDLE') { + return ['Idle State', 'Update State Attribute']; + } else if (msg.currentState === 'RUNNING') { + return ['Running State', 'Update State Attribute']; + } else { + return ['Unknown State']; + } +} +return []; +{:copy-code} +``` + +
+ +You can see real life example, how to use this node in this tutorial: + +- [Data function based on telemetry from 2 devices{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/function-based-on-telemetry-from-two-devices#delta-temperature-rule-chain) + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/transformation_node_script_fn.md b/ui-ngx/src/assets/help/en_US/rulenode/transformation_node_script_fn.md new file mode 100644 index 0000000000..49f246a361 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/rulenode/transformation_node_script_fn.md @@ -0,0 +1,77 @@ +#### Transform message function + +
+
+ +*function Transform(msg, metadata, msgType): {msg: object, metadata: object, msgType: string}* + +The JavaScript function to transform input Message payload, Metadata and/or Message type to the output message. + +**Parameters:** + +{% include rulenode/common_node_script_args %} + +**Returns:** + +Should return the object with the following structure: + +```javascript +{ + msg?: {[key: string]: any}, + metadata?: {[key: string]: string}, + msgType?: string +} +``` + +All fields in resulting object are optional and will be taken from original message if not specified. + +
+ +##### Examples + +* Add sum of two fields ('a' and 'b') as a new field ('sum') of existing message: + +```javascript +if(typeof msg.a !== "undefined" && typeof msg.b !== "undefined"){ + msg.sum = msg.a + msg.b; +} +return {msg: msg}; +``` + +* Transform value of the 'temperature' field from °F to °C: + +```javascript +msg.temperature = (msg.temperature - 32) * 5 / 9; +return {msg: msg}; +``` + +* Replace the incoming message with the new message that contains only one field - count of properties in the original message: + +```javascript +var newMsg = { + count: Object.keys(msg).length +}; +return {msg: newMsg}; +``` + +
    +
  • Change message type to CUSTOM_UPDATE,
    add additional attribute version into payload with value v1.1,
    change sensorType attribute value in Metadata to roomTemp:
  • +
+ +```javascript +var newType = "CUSTOM_UPDATE"; +msg.version = "v1.1"; +metadata.sensorType = "roomTemp" +return {msg: msg, metadata: metadata, msgType: newType}; +{:copy-code} +``` + +
+ +You can see real life example, how to use this node in those tutorials: + +- [Transform incoming telemetry{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/transform-incoming-telemetry/) +- [Reply to RPC Calls{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-transform-script-node) + +
+
diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_action_args.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_action_args.md new file mode 100644 index 0000000000..c8f1b508f6 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_action_args.md @@ -0,0 +1,19 @@ +
  • $event: MouseEvent - The MouseEvent object. Usually a result of a mouse click event. +
  • +
  • widgetContext: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
  • +
  • entityId: string - An optional string id of the target entity. +
  • +
  • entityName: string - An optional string name of the target entity. +
  • +
  • additionalParams: {[key: string]: any} - An optional key/value object holding additional entity parameters. + + +
  • +
  • entityLabel: string - An optional string label of the target entity. +
  • diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md index a71cb598d0..2bd143842d 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md @@ -10,83 +10,72 @@ A JavaScript function performing custom action. **Parameters:**
      -
    • $event: MouseEvent - The MouseEvent object. Usually a result of a mouse click event. -
    • -
    • widgetContext: WidgetContext - A reference to WidgetContext that has all necessary API - and data used by widget instance. -
    • -
    • entityId: string - An optional string id of the target entity. -
    • -
    • entityName: string - An optional string name of the target entity. -
    • - {% include widget/action/custom_additional_params %} -
    • entityLabel: string - An optional string label of the target entity. -
    • + {% include widget/action/custom_action_args %}
    ##### Examples -* Display alert dialog with entity information: - -```javascript -var title; -var content; -if (entityName) { - title = entityName + ' details'; - content = 'Entity name: ' + entityName; - if (additionalParams && additionalParams.entity) { - var entity = additionalParams.entity; - if (entity.id) { - content += '
    Entity type: ' + entity.id.entityType; - } - if (!isNaN(entity.temperature) && entity.temperature !== '') { - content += '
    Temperature: ' + entity.temperature + ' °C'; - } - } -} else { - title = 'No entity information available'; - content = 'No entity information available'; -} - -showAlertDialog(title, content); - -function showAlertDialog(title, content) { - setTimeout(function() { - widgetContext.dialogs.alert(title, content).subscribe(); - }, 100); -} -{:copy-code} -``` - -* Delete device after confirmation: - -```javascript -var $injector = widgetContext.$scope.$injector; -var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); -var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); - -openDeleteDeviceDialog(); - -function openDeleteDeviceDialog() { - var title = 'Are you sure you want to delete the device ' + entityName + '?'; - var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!'; - dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( - function(result) { - if (result) { - deleteDevice(); - } - } - ); -} - -function deleteDevice() { - deviceService.deleteDevice(entityId.id).subscribe( - function() { - widgetContext.updateAliases(); - } - ); -} -{:copy-code} -``` +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md index c71e2b63b8..d13af3fc26 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_additional_params.md @@ -1,46 +1,53 @@ -
  • additionalParams: {[key: string]: any} - An optional key/value object holding additional entity parameters depending on widget type and action source: -
      -
    • Entities table widget (On row click or Action cell button) - additionalParams: { entity: EntityData }: -
        -
      • entity: EntityData - An - EntityData object - presenting basic entity properties (ex. id, entityName) and
        provides access to other entity attributes/timeseries declared in widget datasource configuration. -
      • -
      -
    • -
    • Alarms table widget (On row click or Action cell button) - additionalParams: { alarm: AlarmDataInfo }: -
        -
      • alarm: AlarmDataInfo - An - AlarmDataInfo object - presenting basic alarm properties (ex. type, severity, originator, etc.) and
        provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. -
      • -
      -
    • -
    • Timeseries table widget (On row click or Action cell button) - additionalParams: TimeseriesRow: -
        -
      • additionalParams: TimeseriesRow - A - TimeseriesRow object - presenting formattedTs (a string value of formatted timestamp) and
        timeseries values for each column declared in widget datasource configuration. -
      • -
      -
    • -
    • Entities hierarchy widget (On node selected) - additionalParams: { nodeCtx: HierarchyNodeContext }: -
        -
      • nodeCtx: HierarchyNodeContext - An - HierarchyNodeContext object - containing entity field holding basic entity properties
        (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. -
      • -
      -
    • -
    • Pie - Flot widget (On slice click) - additionalParams: TbFlotPlotItem: -
        -
      • additionalParams: TbFlotPlotItem - A - TbFlotPlotItem object - containing series field with information about datasource and
        data key of clicked pie slice. -
      • -
      -
    • -
    • All other widgets - does not provide additionalParams value. -
    • -
    +#### Additional params object + +
    +
    + +additionalParams: {[key: string]: any} + +An optional key/value object holding additional entity parameters depending on widget type and action source: + +
      +
    • Entities table widget (On row click or Action cell button) - additionalParams: { entity: EntityData }: +
        +
      • entity: EntityData - An + EntityData object + presenting basic entity properties (ex. id, entityName) and
        provides access to other entity attributes/timeseries declared in widget datasource configuration. +
      • +
      +
    • +
    • Alarms table widget (On row click or Action cell button) - additionalParams: { alarm: AlarmDataInfo }: +
        +
      • alarm: AlarmDataInfo - An + AlarmDataInfo object + presenting basic alarm properties (ex. type, severity, originator, etc.) and
        provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. +
      • +
      +
    • +
    • Timeseries table widget (On row click or Action cell button) - additionalParams: TimeseriesRow: +
        +
      • additionalParams: TimeseriesRow - A + TimeseriesRow object + presenting formattedTs (a string value of formatted timestamp) and
        timeseries values for each column declared in widget datasource configuration. +
      • +
      +
    • +
    • Entities hierarchy widget (On node selected) - additionalParams: { nodeCtx: HierarchyNodeContext }: +
        +
      • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
        (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
      • +
      +
    • +
    • Pie - Flot widget (On slice click) - additionalParams: TbFlotPlotItem: +
        +
      • additionalParams: TbFlotPlotItem - A + TbFlotPlotItem object + containing series field with information about datasource and
        data key of clicked pie slice. +
      • +
    • +
    • All other widgets - does not provide additionalParams value. +
    • +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md index a9fea185c3..e82eab1e73 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/custom_pretty_action_fn.md @@ -21,7 +21,14 @@ A JavaScript function performing custom action with defined HTML template to ren
  • htmlTemplate: string - An optional HTML template string defined in HTML tab.
    Used to render custom dialog (see Examples for more details).
  • - {% include widget/action/custom_additional_params %} +
  • additionalParams: {[key: string]: any} - An optional key/value object holding additional entity parameters. + + +
  • entityLabel: string - An optional string label of the target entity.
  • @@ -35,18 +42,20 @@ A JavaScript function performing custom action with defined HTML template to ren

    @@ -55,18 +64,86 @@ A JavaScript function performing custom action with defined HTML template to ren
    +
    + +
    + +
    +
    + +###### Display dialog to created new user + +
    + +
    +
    + +
    + +
    +
    + +###### Display dialog to add/edit image in entity attribute + +
    + +
    +
    + +
    + +
    +
    + +###### Display dialog to clone device + +
    + +

    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md index a604bc3e18..567e093c76 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md @@ -1,6 +1,7 @@ #### HTML template of dialog to create a device or an asset ```html +{:code-style="max-height: 400px;"}
    @@ -158,3 +159,6 @@ {:copy-code} ``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md index 9a904bb944..5dda1b524c 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md @@ -1,6 +1,7 @@ #### Function displaying dialog to create a device or an asset ```javascript +{:code-style="max-height: 400px;"} let $injector = widgetContext.$scope.$injector; let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); @@ -130,3 +131,6 @@ function AddEntityDialogController(instance) { } {:copy-code} ``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md index 2737070a26..4e3cd0c608 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md @@ -1,6 +1,7 @@ #### HTML template of dialog to edit a device or an asset ```html +{:code-style="max-height: 400px;"}
    @@ -190,3 +191,6 @@ {:copy-code} ``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md index 5a222e9aee..b2a6a635df 100644 --- a/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md @@ -1,6 +1,7 @@ #### Function displaying dialog to edit a device or an asset ```javascript +{:code-style="max-height: 400px;"} let $injector = widgetContext.$scope.$injector; let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); @@ -218,3 +219,6 @@ function EditEntityDialogController(instance) { } {:copy-code} ``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_back_first_and_open_state.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_back_first_and_open_state.md new file mode 100644 index 0000000000..c6e8e33473 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_back_first_and_open_state.md @@ -0,0 +1,27 @@ +#### Function go back to the first state, after this go to the target state + +```javascript +{:code-style="max-height: 400px;"} +var stateIndex = widgetContext.stateController.getStateIndex(); +while (stateIndex > 0) { + stateIndex -= 1; + backToPrevState(stateIndex); +} +openDashboardState('devices'); + +function backToPrevState(stateIndex) { + widgetContext.stateController.navigatePrevState(stateIndex); +} + +function openDashboardState(statedId) { + var currentState = widgetContext.stateController.getStateId(); + if (currentState !== statedId) { + var params = {}; + widgetContext.stateController.updateState(statedId, params, false); + } +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_copy_access_token.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_copy_access_token.md new file mode 100644 index 0000000000..975883f7f8 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_copy_access_token.md @@ -0,0 +1,44 @@ +#### Function copy device access token to buffer + +```javascript +{:code-style="max-height: 400px;"} +var $injector = widgetContext.$scope.$injector; +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +var $translate = $injector.get(widgetContext.servicesMap.get('translate')); +var $scope = widgetContext.$scope; +if (entityId.id && entityId.entityType === 'DEVICE') { + deviceService.getDeviceCredentials(entityId.id, true).subscribe( + (deviceCredentials) => { + var credentialsId = deviceCredentials.credentialsId; + if (copyToClipboard(credentialsId)) { + $scope.showSuccessToast($translate.instant('device.accessTokenCopiedMessage'), 750, "top", "left"); + } + } + ); +} + +function copyToClipboard(text) { + if (window.clipboardData && window.clipboardData.setData) { + return window.clipboardData.setData("Text", text); + } + else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { + var textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.position = "fixed"; + document.body.appendChild(textarea); + textarea.select(); + try { + return document.execCommand("copy"); + } + catch (ex) { + console.warn("Copy to clipboard failed.", ex); + return false; + } + document.body.removeChild(textarea); + } +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_delete_device_confirm.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_delete_device_confirm.md new file mode 100644 index 0000000000..83ba86c348 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_delete_device_confirm.md @@ -0,0 +1,33 @@ +#### Function delete device after confirmation + +```javascript +var $injector = widgetContext.$scope.$injector; +var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs')); +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); + +openDeleteDeviceDialog(); + +function openDeleteDeviceDialog() { + var title = 'Are you sure you want to delete the device ' + entityName + '?'; + var content = 'Be careful, after the confirmation, the device and all related data will become unrecoverable!'; + dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe( + function(result) { + if (result) { + deleteDevice(); + } + } + ); +} + +function deleteDevice() { + deviceService.deleteDevice(entityId.id).subscribe( + function() { + widgetContext.updateAliases(); + } + ); +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_display_alert.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_display_alert.md new file mode 100644 index 0000000000..bf6ed2b6fd --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_display_alert.md @@ -0,0 +1,35 @@ +#### Function display alert dialog with entity information + +```javascript +{:code-style="max-height: 400px;"} +var title; +var content; +if (entityName) { + title = entityName + ' details'; + content = 'Entity name: ' + entityName; + if (additionalParams && additionalParams.entity) { + var entity = additionalParams.entity; + if (entity.id) { + content += '
    Entity type: ' + entity.id.entityType; + } + if (!isNaN(entity.temperature) && entity.temperature !== '') { + content += '
    Temperature: ' + entity.temperature + ' °C'; + } + } +} else { + title = 'No entity information available'; + content = 'No entity information available'; +} + +showAlertDialog(title, content); + +function showAlertDialog(title, content) { + setTimeout(function() { + widgetContext.dialogs.alert(title, content).subscribe(); + }, 100); +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_open_state_save_parameters.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_open_state_save_parameters.md new file mode 100644 index 0000000000..18bd3f53ab --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_open_state_save_parameters.md @@ -0,0 +1,34 @@ +#### Function open state conditionally with saving particular state parameters + +```javascript +{:code-style="max-height: 400px;"} +var entitySubType; +var $injector = widgetContext.$scope.$injector; +$injector.get(widgetContext.servicesMap.get('entityService')).getEntity(entityId.entityType, entityId.id) + .subscribe(function(data) { + entitySubType = data.type; + if (entitySubType == 'energy meter') { + openDashboardStates('energy_meter_details_view'); + } else if (entitySubType == 'thermometer') { + openDashboardStates('thermometer_details_view'); + } + }); + +function openDashboardStates(statedId) { + var stateParams = widgetContext.stateController.getStateParams(); + var params = { + entityId: entityId, + entityName: entityName + }; + + if (stateParams.city) { + params.city = stateParams.city; + } + + widgetContext.stateController.openState(statedId, params, false); +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_return_previous_state.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_return_previous_state.md new file mode 100644 index 0000000000..77f65edbe7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_action/custom_action_return_previous_state.md @@ -0,0 +1,18 @@ +#### Function return to the previous state + +```javascript +{:code-style="max-height: 400px;"} +let stateIndex = widgetContext.stateController.getStateIndex(); +if (stateIndex > 0) { + stateIndex -= 1; + backToPrevState(stateIndex); +} + +function backToPrevState(stateIndex) { + widgetContext.stateController.navigatePrevState(stateIndex); +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_html.md new file mode 100644 index 0000000000..7f93d6266b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_html.md @@ -0,0 +1,46 @@ +#### HTML template of dialog to clone device + +```html +{:code-style="max-height: 400px;"} +
    + +

    Clone device: {{ deviceName }}

    + + +
    + + +
    +
    + + Clone device name + + + Clone device name is required + + +
    +
    + + +
    +
    +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_js.md new file mode 100644 index 0000000000..2ee9160cce --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_clone_device_js.md @@ -0,0 +1,56 @@ +#### Function displaying dialog to clone device + +```javascript +{:code-style="max-height: 400px;"} +const $injector = widgetContext.$scope.$injector; +const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +const attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); +const deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +const rxjs = widgetContext.rxjs; + +openCloneDeviceDialog(); + +function openCloneDeviceDialog() { + customDialog.customDialog(htmlTemplate, CloneDeviceDialogController).subscribe(); +} + +function CloneDeviceDialogController(instance) { + let vm = instance; + vm.deviceName = entityName; + + vm.cloneDeviceFormGroup = vm.fb.group({ + cloneName: ['', [vm.validators.required]] + }); + + vm.save = function() { + deviceService.getDevice(entityId.id).pipe( + rxjs.mergeMap((origDevice) => { + let cloneDevice = { + name: vm.cloneDeviceFormGroup.get('cloneName').value, + type: origDevice.type + }; + return deviceService.saveDevice(cloneDevice).pipe( + rxjs.mergeMap((newDevice) => { + return attributeService.getEntityAttributes(origDevice.id, 'SERVER_SCOPE').pipe( + rxjs.mergeMap((origAttributes) => { + return attributeService.saveEntityAttributes(newDevice.id, 'SERVER_SCOPE', origAttributes); + }) + ); + }) + ); + }) + ).subscribe(() => { + widgetContext.updateAliases(); + vm.dialogRef.close(null); + }); + }; + + vm.cancel = function() { + vm.dialogRef.close(null); + }; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md new file mode 100644 index 0000000000..567e093c76 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_html.md @@ -0,0 +1,164 @@ +#### HTML template of dialog to create a device or an asset + +```html +{:code-style="max-height: 400px;"} +
    + +

    Add entity

    + + +
    + + +
    +
    +
    + + Entity Name + + + Entity name is required. + + + + Entity Label + + +
    +
    + + + +
    +
    +
    + + Latitude + + + + Longitude + + +
    +
    + + Address + + + + Owner + + +
    +
    + + Integer Value + + + Invalid integer value. + + +
    + + + + +
    +
    +
    +
    +
    Relations
    +
    +
    +
    +
    +
    + + Direction + + + + + + + Relation direction is required. + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_js.md new file mode 100644 index 0000000000..5dda1b524c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_dialog_js.md @@ -0,0 +1,136 @@ +#### Function displaying dialog to create a device or an asset + +```javascript +{:code-style="max-height: 400px;"} +let $injector = widgetContext.$scope.$injector; +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); + +openAddEntityDialog(); + +function openAddEntityDialog() { + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(); +} + +function AddEntityDialogController(instance) { + let vm = instance; + + vm.allowedEntityTypes = ['ASSET', 'DEVICE']; + vm.entitySearchDirection = { + from: "FROM", + to: "TO" + } + + vm.addEntityFormGroup = vm.fb.group({ + entityName: ['', [vm.validators.required]], + entityType: ['DEVICE'], + entityLabel: [null], + type: ['', [vm.validators.required]], + attributes: vm.fb.group({ + latitude: [null], + longitude: [null], + address: [null], + owner: [null], + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], + booleanValue: [null] + }), + relations: vm.fb.array([]) + }); + + vm.cancel = function () { + vm.dialogRef.close(null); + }; + + vm.relations = function () { + return vm.addEntityFormGroup.get('relations'); + }; + + vm.addRelation = function () { + vm.relations().push(vm.fb.group({ + relatedEntity: [null, [vm.validators.required]], + relationType: [null, [vm.validators.required]], + direction: [null, [vm.validators.required]] + })); + }; + + vm.removeRelation = function (index) { + vm.relations().removeAt(index); + vm.relations().markAsDirty(); + }; + + vm.save = function () { + vm.addEntityFormGroup.markAsPristine(); + saveEntityObservable().subscribe( + function (entity) { + widgetContext.rxjs.forkJoin([ + saveAttributes(entity.id), + saveRelations(entity.id) + ]).subscribe( + function () { + widgetContext.updateAliases(); + vm.dialogRef.close(null); + } + ); + } + ); + }; + + function saveEntityObservable() { + const formValues = vm.addEntityFormGroup.value; + let entity = { + name: formValues.entityName, + type: formValues.type, + label: formValues.entityLabel + }; + if (formValues.entityType == 'ASSET') { + return assetService.saveAsset(entity); + } else if (formValues.entityType == 'DEVICE') { + return deviceService.saveDevice(entity); + } + } + + function saveAttributes(entityId) { + let attributes = vm.addEntityFormGroup.get('attributes').value; + let attributesArray = []; + for (let key in attributes) { + if (attributes[key] !== null) { + attributesArray.push({key: key, value: attributes[key]}); + } + } + if (attributesArray.length > 0) { + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); + } + return widgetContext.rxjs.of([]); + } + + function saveRelations(entityId) { + let relations = vm.addEntityFormGroup.get('relations').value; + let tasks = []; + for (let i = 0; i < relations.length; i++) { + let relation = { + type: relations[i].relationType, + typeGroup: 'COMMON' + }; + if (relations[i].direction == 'FROM') { + relation.to = relations[i].relatedEntity; + relation.from = entityId; + } else { + relation.to = entityId; + relation.from = relations[i].relatedEntity; + } + tasks.push(entityRelationService.saveRelation(relation)); + } + if (tasks.length > 0) { + return widgetContext.rxjs.forkJoin(tasks); + } + return widgetContext.rxjs.of([]); + } +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_html.md new file mode 100644 index 0000000000..c523258fbc --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_html.md @@ -0,0 +1,81 @@ +#### HTML template of dialog to create new user + +```html +{:code-style="max-height: 400px;"} +
    + +

    Add new User

    + + +
    + + +
    +
    +
    +
    + + Email + + + Email is required + + + Invalid email format + + +
    +
    + + First Name + + +
    +
    + + Last Name + + +
    +
    + + User activation method + + + {{activationMethod.name}} + + + Please choose activation method + e.g. Send activation email + +
    +
    +
    + + +
    +
    +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md new file mode 100644 index 0000000000..22738c53c1 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_create_user_js.md @@ -0,0 +1,137 @@ +#### Function displaying dialog to create new user + +```javascript +{:code-style="max-height: 400px;"} +const $injector = widgetContext.$scope.$injector; +const customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +const userService = $injector.get(widgetContext.servicesMap.get('userService')); +const $scope = widgetContext.$scope; +const rxjs = widgetContext.rxjs; + +openAddUserDialog(); + +function openAddUserDialog() { + customDialog.customDialog(htmlTemplate, AddUserDialogController).subscribe(); +} + +function AddUserDialogController(instance) { + let vm = instance; + + vm.currentUser = widgetContext.currentUser; + + vm.addEntityFormGroup = vm.fb.group({ + email: ['', [vm.validators.required, vm.validators.pattern(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\_\-0-9]+\.)+[a-zA-Z]{2,}))$/)]], + firstName: [''], + lastName: ['', ], + userActivationMethod: ['', [vm.validators.required]] + }); + + vm.activationMethods = [ + { + value: 'displayActivationLink', + name: 'Display activation link' + }, + { + value: 'sendActivationMail', + name: 'Send activation email' + } + ]; + + vm.cancel = function() { + vm.dialogRef.close(null); + }; + + vm.save = function() { + let formObj = vm.addEntityFormGroup.getRawValue(); + let attributes = []; + let sendActivationMail = false; + let newUser = { + email: formObj.email, + firstName: formObj.firstName, + lastName: formObj.lastName, + authority: 'TENANT_ADMIN' + }; + + if (formObj.userActivationMethod === 'sendActivationMail') { + sendActivationMail = true; + } + + userService.saveUser(newUser, sendActivationMail).pipe( + rxjs.mergeMap((user) => { + let activationObs; + if (sendActivationMail) { + activationObs = rxjs.of(null); + } else { + activationObs = userService.getActivationLink(user.id.id); + } + return activationObs.pipe( + rxjs.mergeMap((activationLink) => { + return activationLink ? customDialog.customDialog(activationLinkDialogTemplate, ActivationLinkDialogController, {"activationLink": activationLink}) : rxjs.of(null); + }) + ); + }) + ).subscribe(() => { + vm.dialogRef.close(null); + }); + }; +} + +function ActivationLinkDialogController(instance) { + let vm = instance; + + vm.activationLink = vm.data.activationLink; + + vm.onActivationLinkCopied = () => { + $scope.showSuccessToast("User activation link has been copied to clipboard", 1200, "bottom", "left", "activationLinkDialogContent"); + }; + + vm.close = () => { + vm.dialogRef.close(null); + }; +} + +let activationLinkDialogTemplate = `
    + +

    user.activation-link

    + + +
    + + +
    +
    +
    + +
    +
    {{ activationLink }}
    + +
    +
    +
    +
    + +
    +
    `; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md new file mode 100644 index 0000000000..4e3cd0c608 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_html.md @@ -0,0 +1,196 @@ +#### HTML template of dialog to edit a device or an asset + +```html +{:code-style="max-height: 400px;"} +
    + +

    Edit

    + + +
    + + +
    +
    +
    + + Entity Name + + + + Entity Label + + +
    +
    + + Entity Type + + + + Type + + +
    +
    +
    + + Latitude + + + + Longitude + + +
    +
    + + Address + + + + Owner + + +
    +
    + + Integer Value + + + Invalid integer value. + + +
    + + + + +
    +
    +
    +
    +
    Relations
    +
    +
    +
    +
    +
    + + Direction + + + + + + + Relation direction is required. + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    New Relations
    +
    +
    +
    +
    +
    + + Direction + + + + + + + Relation direction is required. + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + +
    +
    +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md new file mode 100644 index 0000000000..b2a6a635df --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_dialog_js.md @@ -0,0 +1,224 @@ +#### Function displaying dialog to edit a device or an asset + +```javascript +{:code-style="max-height: 400px;"} +let $injector = widgetContext.$scope.$injector; +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); +let entityRelationService = $injector.get(widgetContext.servicesMap.get('entityRelationService')); + +openEditEntityDialog(); + +function openEditEntityDialog() { + customDialog.customDialog(htmlTemplate, EditEntityDialogController).subscribe(); +} + +function EditEntityDialogController(instance) { + let vm = instance; + + vm.entityName = entityName; + vm.entityType = entityId.entityType; + vm.entitySearchDirection = { + from: "FROM", + to: "TO" + }; + vm.attributes = {}; + vm.oldRelationsData = []; + vm.relationsToDelete = []; + vm.entity = {}; + + vm.editEntityFormGroup = vm.fb.group({ + entityName: ['', [vm.validators.required]], + entityType: [null], + entityLabel: [null], + type: ['', [vm.validators.required]], + attributes: vm.fb.group({ + latitude: [null], + longitude: [null], + address: [null], + owner: [null], + number: [null, [vm.validators.pattern(/^-?[0-9]+$/)]], + booleanValue: [false] + }), + oldRelations: vm.fb.array([]), + relations: vm.fb.array([]) + }); + + getEntityInfo(); + + vm.cancel = function() { + vm.dialogRef.close(null); + }; + + vm.relations = function() { + return vm.editEntityFormGroup.get('relations'); + }; + + vm.oldRelations = function() { + return vm.editEntityFormGroup.get('oldRelations'); + }; + + vm.addRelation = function() { + vm.relations().push(vm.fb.group({ + relatedEntity: [null, [vm.validators.required]], + relationType: [null, [vm.validators.required]], + direction: [null, [vm.validators.required]] + })); + }; + + function addOldRelation() { + vm.oldRelations().push(vm.fb.group({ + relatedEntity: [{value: null, disabled: true}, [vm.validators.required]], + relationType: [{value: null, disabled: true}, [vm.validators.required]], + direction: [{value: null, disabled: true}, [vm.validators.required]] + })); + } + + vm.removeRelation = function(index) { + vm.relations().removeAt(index); + vm.relations().markAsDirty(); + }; + + vm.removeOldRelation = function(index) { + vm.oldRelations().removeAt(index); + vm.relationsToDelete.push(vm.oldRelationsData[index]); + vm.oldRelations().markAsDirty(); + }; + + vm.save = function() { + vm.editEntityFormGroup.markAsPristine(); + widgetContext.rxjs.forkJoin([ + saveAttributes(entityId), + saveRelations(entityId), + saveEntity() + ]).subscribe( + function () { + widgetContext.updateAliases(); + vm.dialogRef.close(null); + } + ); + }; + + function getEntityAttributes(attributes) { + for (var i = 0; i < attributes.length; i++) { + vm.attributes[attributes[i].key] = attributes[i].value; + } + } + + function getEntityRelations(relations) { + let relationsFrom = relations[0]; + let relationsTo = relations[1]; + for (let i=0; i < relationsFrom.length; i++) { + let relation = { + direction: 'FROM', + relationType: relationsFrom[i].type, + relatedEntity: relationsFrom[i].to + }; + vm.oldRelationsData.push(relation); + addOldRelation(); + } + for (let i=0; i < relationsTo.length; i++) { + let relation = { + direction: 'TO', + relationType: relationsTo[i].type, + relatedEntity: relationsTo[i].from + }; + vm.oldRelationsData.push(relation); + addOldRelation(); + } + } + + function getEntityInfo() { + widgetContext.rxjs.forkJoin([ + entityRelationService.findInfoByFrom(entityId), + entityRelationService.findInfoByTo(entityId), + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE'), + entityService.getEntity(entityId.entityType, entityId.id) + ]).subscribe( + function (data) { + getEntityRelations(data.slice(0,2)); + getEntityAttributes(data[2]); + vm.entity = data[3]; + vm.editEntityFormGroup.patchValue({ + entityName: vm.entity.name, + entityType: vm.entityType, + entityLabel: vm.entity.label, + type: vm.entity.type, + attributes: vm.attributes, + oldRelations: vm.oldRelationsData + }, {emitEvent: false}); + } + ); + } + + function saveEntity() { + const formValues = vm.editEntityFormGroup.value; + if (vm.entity.label !== formValues.entityLabel){ + vm.entity.label = formValues.entityLabel; + if (formValues.entityType == 'ASSET') { + return assetService.saveAsset(vm.entity); + } else if (formValues.entityType == 'DEVICE') { + return deviceService.saveDevice(vm.entity); + } + } + return widgetContext.rxjs.of([]); + } + + function saveAttributes(entityId) { + let attributes = vm.editEntityFormGroup.get('attributes').value; + let attributesArray = []; + for (let key in attributes) { + if (attributes[key] !== vm.attributes[key]) { + attributesArray.push({key: key, value: attributes[key]}); + } + } + if (attributesArray.length > 0) { + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); + } + return widgetContext.rxjs.of([]); + } + + function saveRelations(entityId) { + let relations = vm.editEntityFormGroup.get('relations').value; + let tasks = []; + for(let i=0; i < relations.length; i++) { + let relation = { + type: relations[i].relationType, + typeGroup: 'COMMON' + }; + if (relations[i].direction == 'FROM') { + relation.to = relations[i].relatedEntity; + relation.from = entityId; + } else { + relation.to = entityId; + relation.from = relations[i].relatedEntity; + } + tasks.push(entityRelationService.saveRelation(relation)); + } + for (let i=0; i < vm.relationsToDelete.length; i++) { + let relation = { + type: vm.relationsToDelete[i].relationType + }; + if (vm.relationsToDelete[i].direction == 'FROM') { + relation.to = vm.relationsToDelete[i].relatedEntity; + relation.from = entityId; + } else { + relation.to = entityId; + relation.from = vm.relationsToDelete[i].relatedEntity; + } + tasks.push(entityRelationService.deleteRelation(relation.from, relation.type, relation.to)); + } + if (tasks.length > 0) { + return widgetContext.rxjs.forkJoin(tasks); + } + return widgetContext.rxjs.of([]); + } +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_html.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_html.md new file mode 100644 index 0000000000..3f2eff11c6 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_html.md @@ -0,0 +1,42 @@ +#### HTML template of dialog to add/edit image in entity attribute + +```html +{:code-style="max-height: 400px;"} +
    + +

    Edit {{entityName}} image

    + + +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_js.md b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_js.md new file mode 100644 index 0000000000..21acb89321 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/examples_custom_pretty/custom_pretty_edit_image_js.md @@ -0,0 +1,85 @@ +#### Function displaying dialog to add/edit image in entity attribute + +```javascript +{:code-style="max-height: 400px;"} +let $injector = widgetContext.$scope.$injector; +let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog')); +let assetService = $injector.get(widgetContext.servicesMap.get('assetService')); +let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService')); +let entityService = $injector.get(widgetContext.servicesMap.get('entityService')); + +openAddEntityDialog(); + +function openAddEntityDialog() { + customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe(() => {}); +} + +function AddEntityDialogController(instance) { + let vm = instance; + + vm.entityName = entityName; + + vm.attributes = {}; + + vm.editEntity = vm.fb.group({ + attributes: vm.fb.group({ + image: [null] + }) + }); + + getEntityInfo(); + + vm.cancel = function() { + vm.dialogRef.close(null); + }; + + vm.save = function() { + vm.loading = true; + saveAttributes(entityId).subscribe( + () => { + vm.dialogRef.close(null); + }, () =>{ + vm.loading = false; + } + ); + }; + + function getEntityAttributes(attributes) { + for (var i = 0; i < attributes.length; i++) { + vm.attributes[attributes[i].key] = attributes[i].value; + } + } + + function getEntityInfo() { + vm.loading = true; + attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE').subscribe( + function (data) { + getEntityAttributes(data); + + vm.editEntity.patchValue({ + attributes: vm.attributes + }, {emitEvent: false}); + vm.loading = false; + } + ); + } + + function saveAttributes(entityId) { + let attributes = vm.editEntity.get('attributes').value; + let attributesArray = []; + for (let key in attributes) { + if (attributes[key] !== vm.attributes[key]) { + attributesArray.push({key: key, value: attributes[key]}); + } + } + if (attributesArray.length > 0) { + return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray); + } + return widgetContext.rxjs.of([]); + } +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_get_location_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_get_location_fn.md new file mode 100644 index 0000000000..c504ac1b67 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_get_location_fn.md @@ -0,0 +1,49 @@ +#### Get location function + +
    +
    + +*function getLocation($event, widgetContext, entityId, entityName, additionalParams, entityLabel): [number, number] | Observable<[number, number]>* + +A JavaScript function that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.
    +Usually location can be obtained from entity attributes/telemetry. + +**Parameters:** + +
      + {% include widget/action/custom_action_args %} +
    + +**Returns:** + +Latitude and longitude as array of two numbers or Observable of array of two numbers. For example ```[37.689, -122.433]```. + +
    + +##### Examples + +* Return location from entity attributes: + +```javascript +return getLocationFromEntityAttributes(); + +function getLocationFromEntityAttributes() { + if (entityId) { + return widgetContext.attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE', ['latitude', 'longitude']) + .pipe(widgetContext.rxjs + .map(function(attributeData) { + var res = [0,0]; + if (attributeData && attributeData.length === 2) { + res[0] = attributeData.filter(function (data) { return data.key === 'latitude'})[0].value; + res[1] = attributeData.filter(function (data) { return data.key === 'longitude'})[0].value; + } + return res; + } + ) + ); + } else { + return [0,0]; + } +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_get_phone_number_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_get_phone_number_fn.md new file mode 100644 index 0000000000..752d9a3909 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_get_phone_number_fn.md @@ -0,0 +1,48 @@ +#### Get phone number function + +
    +
    + +*function getPhoneNumber($event, widgetContext, entityId, entityName, additionalParams, entityLabel): number | string | Observable<number> | Observable<string>* + +A JavaScript function that should return phone number for further processing by mobile action.
    +Usually phone number can be obtained from entity attributes/telemetry. + +**Parameters:** + +
      + {% include widget/action/custom_action_args %} +
    + +**Returns:** + +String or numeric value of phone number or Observable of string or numeric value. For example ```123456789```. + +
    + +##### Examples + +* Return phone number from entity attributes: + +```javascript +return getPhoneNumberFromEntityAttributes(); + +function getPhoneNumberFromEntityAttributes() { + if (entityId) { + return widgetContext.attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE', ['phone']) + .pipe(widgetContext.rxjs + .map(function(attributeData) { + var res = 0; + if (attributeData && attributeData.length === 1) { + res = attributeData[0].value; + } + return res; + } + ) + ); + } else { + return 0; + } +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_empty_result_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_empty_result_fn.md new file mode 100644 index 0000000000..9722c3ea5a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_empty_result_fn.md @@ -0,0 +1,31 @@ +#### Handle empty result function + +
    +
    + +*function handleEmptyResult($event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* + +An optional JavaScript function to handle empty result.
    Usually this happens when user cancels the action (for ex. by pressing phone back button). + +**Parameters:** + +
      + {% include widget/action/custom_action_args %} +
    + +
    + +##### Examples + +* Display alert dialog with canceled action message: + +```javascript +showEmptyResultDialog('Action was canceled!'); + +function showEmptyResultDialog(message) { + setTimeout(function() { + widgetContext.dialogs.alert('Empty result', message).subscribe(); + }, 100); +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_error_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_error_fn.md new file mode 100644 index 0000000000..01bf76c8fd --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_handle_error_fn.md @@ -0,0 +1,33 @@ +#### Handle error function + +
    +
    + +*function handleError(error, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* + +An optional JavaScript function to handle error occurred while mobile action execution. + +**Parameters:** + +
      +
    • error: string - error message. +
    • + {% include widget/action/custom_action_args %} +
    + +
    + +##### Examples + +* Display alert dialog with error message: + +```javascript +showErrorDialog('Failed to perform action', error); + +function showErrorDialog(title, error) { + setTimeout(function() { + widgetContext.dialogs.alert(title, error).subscribe(); + }, 100); +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_image_fn.md new file mode 100644 index 0000000000..5759571fe7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_image_fn.md @@ -0,0 +1,92 @@ +#### Process image function + +
    +
    + +*function processImage(imageUrl, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* + +A JavaScript function to process image obtained as a result of mobile action (take photo, take image from gallery, etc.). + +**Parameters:** + +
      +
    • imageUrl: string - An image URL in base64 data format. +
    • + {% include widget/action/custom_action_args %} +
    + +
    + +##### Examples + +* Store image url data to entity attribute: + +```javascript +saveEntityImageAttribute('image', imageUrl); + +function saveEntityImageAttribute(attributeName, imageUrl) { + if (entityId) { + let attributes = [{ + key: attributeName, value: imageUrl + }]; + widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe( + function() { + widgetContext.showSuccessToast('Image attribute saved!'); + }, + function(error) { + widgetContext.dialogs.alert('Image attribute save failed', JSON.stringify(error)); + } + ); + } +} +{:copy-code} +``` + +* Display dialog with obtained image: + +```javascript +showImageDialog('Image', imageUrl); + +function showImageDialog(title, imageUrl) { + setTimeout(function() { + widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe(); + }, 100); +} + +var imageDialogTemplate = + '
    ' + + '
    ' + + '' + + '

    {{title}}

    ' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '' + + '
    ' + + '
    ' + + '
    '; + +function ImageDialogController(instance) { + let vm = instance; + vm.title = vm.data.title; + vm.imageUrl = vm.data.imageUrl; + vm.close = function () + { + vm.dialogRef.close(null); + } +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_launch_result_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_launch_result_fn.md new file mode 100644 index 0000000000..ddcab07e18 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_launch_result_fn.md @@ -0,0 +1,33 @@ +#### Process launch result function + +
    +
    + +*function processLaunchResult(launched, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* + +An optional JavaScript function to process result of attempt to launch external mobile application (for ex. map application or phone call application). + +**Parameters:** + +
      +
    • launched: boolean - boolean value indicating if the external application was successfully launched. + {% include widget/action/custom_action_args %} +
    + +
    + +##### Examples + +* Display alert dialog with external application launch status: + +```javascript +showLaunchStatusDialog('Application', launched); + +function showLaunchStatusDialog(title, status) { + setTimeout(function() { + widgetContext.dialogs.alert(title, status ? 'Successfully launched' : 'Failed to launch').subscribe(); + }, 100); +} + +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_location_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_location_fn.md new file mode 100644 index 0000000000..445053b1dd --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_location_fn.md @@ -0,0 +1,59 @@ +#### Process location function + +
    +
    + +*function processLocation(latitude, longitude, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* + +A JavaScript function to process current location of the phone. + +**Parameters:** + +
      +
    • latitude: number - phone location latitude. +
    • +
    • longitude: number - phone location longitude. +
    • + {% include widget/action/custom_action_args %} +
    + +
    + +##### Examples + +* Display alert dialog with location data: + +```javascript +showLocationDialog('Location', latitude, longitude); + +function showLocationDialog(title, latitude, longitude) { + setTimeout(function() { + widgetContext.dialogs.alert(title, 'Latitude: '+latitude+'
    Longitude: ' + longitude).subscribe(); + }, 100); +} +{:copy-code} +``` + +* Store phone location to entity attributes: + +```javascript +saveEntityLocationAttributes('latitude', 'longitude', latitude, longitude); + +function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) { + if (entityId) { + let attributes = [ + { key: latitudeAttributeName, value: latitude }, + { key: longitudeAttributeName, value: longitude } + ]; + widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe( + function() { + widgetContext.showSuccessToast('Location attributes saved!'); + }, + function(error) { + widgetContext.dialogs.alert('Location attributes save failed', JSON.stringify(error)); + } + ); + } +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md new file mode 100644 index 0000000000..bb663f9e7d --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md @@ -0,0 +1,76 @@ +#### Process QR code function + +
    +
    + +*function processQrCode(code, format, $event, widgetContext, entityId, entityName, additionalParams, entityLabel): void* + +A JavaScript function to process result of barcode scanning. + +**Parameters:** + +
      +
    • code: string - A string value of scanned barcode. +
    • +
    • format: string - barcode format. See BarcodeFormat enum for possible values. +
    • + {% include widget/action/custom_action_args %} +
    + +
    + +##### Examples + +* Display alert dialog with scanned barcode: + +```javascript +showQrCodeDialog('Bar Code', code, format); + +function showQrCodeDialog(title, code, format) { + setTimeout(function() { + widgetContext.dialogs.alert(title, 'Code: ['+code+']
    Format: ' + format).subscribe(); + }, 100); +} +{:copy-code} +``` + +* Parse code as a device claiming info (in this case ```{deviceName: string, secretKey: string}```)
    and then claim device (see [Claiming devices{:target="_blank"}](${siteBaseUrl}/docs/user-guide/claiming-devices/) for details): + +```javascript +var $scope = widgetContext.$scope; +var $injector = $scope.$injector; +var $translate = $injector.get(widgetContext.servicesMap.get('translate')); +var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService')); +var deviceNotFound = $translate.instant('widgets.input-widgets.claim-not-found'); +var failedClaimDevice = $translate.instant('widgets.input-widgets.claim-failed'); +var claimDeviceInfo = JSON.parse(code); +var deviceName = claimDeviceInfo.deviceName; +var secretKey = claimDeviceInfo.secretKey; +var claimRequest = { + secretKey: secretKey +}; +deviceService.claimDevice(deviceName, claimRequest, { ignoreErrors: true }).subscribe( + function (data) { + widgetContext.showSuccessToast('Device \'' + deviceName + '\' successfully claimed!'); + widgetContext.updateAliases(); + }, + function (error) { + if(error.status == 404) { + widgetContext.showErrorToast(deviceNotFound); + } else { + if (error.status !== 400 && error.error && error.error.message) { + showDialog('Failed to claim device', error.error.message); + } else { + widgetContext.showErrorToast(failedClaimDevice); + } + } + } +); + +function showDialog(title, error) { + setTimeout(function() { + widgetContext.dialogs.alert(title, error).subscribe(); + }, 100); +} +{:copy-code} +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md b/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md index 3dbece51c6..b73ec7bfe8 100644 --- a/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/config/datakey_generation_fn.md @@ -57,3 +57,6 @@ var index = Math.floor((time/3 % 14000) / 1000); return lats[index]; {:copy-code} ``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md b/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md index 6fd0cf867f..fc4999757a 100644 --- a/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/config/datakey_postprocess_fn.md @@ -54,3 +54,33 @@ if (prevOrigValue) { } {:copy-code} ``` +* Formatting data to time format + +```javascript +if (value) { + return moment(value).format("DD/MM/YYYY HH:mm:ss"); +} +return ''; +{:copy-code} +``` + +* Creates line-breaks for 0 values, when used in line chart + +```javascript +if (value === 0) { + return null; +} else { + return value; +} +{:copy-code} +``` + +* Display data point of the HTML value card under the condition + +```javascript +return value ? '
    Temperature: '+value+' °C
    ' : ''; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md new file mode 100644 index 0000000000..f3ba4c0590 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md @@ -0,0 +1,147 @@ +#### Sample Alarm widget + +
    +
    + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.
    +Click the **Alarm Widget** button on the **Select widget type** popup.
    +The **Widget Editor** will be opened, pre-populated with the content of the default **Alarm** template widget. + + - Replace content of the CSS tab in "Resources" section with the following one: + +```css +.my-alarm-table th { + text-align: left; +} +{:copy-code} +``` + + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html +
    +
    My first Alarm widget.
    + + + + + + + + + + + +
    {{dataKey.label}}
    + {{getAlarmValue(alarm, dataKey)}} +
    +
    +{:copy-code} +``` + + - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + +```json +{ + "schema": { + "type": "object", + "title": "AlarmTableSettings", + "properties": { + "alarmSeverityColorFunction": { + "title": "Alarm severity color function: f(severity)", + "type": "string", + "default": "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green'; " + } + }, + "required": [] + }, + "form": [ + { + "key": "alarmSeverityColorFunction", + "type": "javascript" + } + ] +} +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript +self.onInit = function() { + var pageLink = self.ctx.pageLink(); + + pageLink.typeList = self.ctx.widgetConfig.alarmTypeList; + pageLink.statusList = self.ctx.widgetConfig.alarmStatusList; + pageLink.severityList = self.ctx.widgetConfig.alarmSeverityList; + pageLink.searchPropagatedAlarms = self.ctx.widgetConfig.searchPropagatedAlarms; + + self.ctx.defaultSubscription.subscribeForAlarms(pageLink, null); + self.ctx.$scope.alarmSource = self.ctx.defaultSubscription.alarmSource; + + var alarmSeverityColorFunctionBody = self.ctx.settings.alarmSeverityColorFunction; + if (typeof alarmSeverityColorFunctionBody === 'undefined' || !alarmSeverityColorFunctionBody.length) { + alarmSeverityColorFunctionBody = "if(severity == 'CRITICAL') {return 'red';} else if (severity == 'MAJOR') {return 'orange';} else return 'green';"; + } + + var alarmSeverityColorFunction = null; + try { + alarmSeverityColorFunction = new Function('severity', alarmSeverityColorFunctionBody); + } catch (e) { + alarmSeverityColorFunction = null; + } + + self.ctx.$scope.getAlarmValue = function(alarm, dataKey) { + var alarmKey = dataKey.name; + if (alarmKey === 'originator') { + alarmKey = 'originatorName'; + } + var value = alarm[alarmKey]; + if (alarmKey === 'createdTime') { + return self.ctx.date.transform(value, 'yyyy-MM-dd HH:mm:ss'); + } else { + return value; + } + } + + self.ctx.$scope.getAlarmCellStyle = function(alarm, dataKey) { + var alarmKey = dataKey.name; + if (alarmKey === 'severity' && alarmSeverityColorFunction) { + var severity = alarm[alarmKey]; + var color = alarmSeverityColorFunction(severity); + return { + color: color + }; + } + return {}; + } +} + +self.onDataUpdated = function() { + self.ctx.$scope.alarms = self.ctx.defaultSubscription.alarms.data; + self.ctx.detectChanges(); +} +{:copy-code} +``` + + - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/alarm-widget-sample.png) + +In this example, the **alarmSource** and **alarms** properties of are assigned to **$scope** and become accessible within HTML template. + +Inside the HTML, a special [***ngFor**{:target="_blank"}](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available alarm **dataKeys** of **alarmSource** and render corresponding columns. + +The table rows are rendered by iterating over **alarms** array and corresponding cells rendered by iterating over **dataKeys**. + +The function **getAlarmValue** is fetching alarm value and formatting **createdTime** alarm property using a [DatePipe{:target="_blank"}](https://angular.io/api/common/DatePipe) angular pipe accessible via **date** property of **ctx**. + +The function **getAlarmCellStyle** is used to assign custom cell styles for each alarm cell.
    In this example, we introduced new settings property called **alarmSeverityColorFunction** that contains function body returning color depending on alarm severity. + +Inside the **getAlarmCellStyle** function there is corresponding invocation of **alarmSeverityColorFunction** with severity value in order to get color for alarm severity cell. + +Note that in this code **onDataUpdated** function is implemented in order to update **alarms** property with latest alarms from subscription and invoke change detection using **detectChanges()** function. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md new file mode 100644 index 0000000000..dcb9293f84 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md @@ -0,0 +1,67 @@ +#### Latest Values widget Example with gauge.js library + +
    +
    + +In this example, **Latest Values** gauge widget will be created using external [gauge.js{:target="_blank"}](http://bernii.github.io/gauge.js/) library. + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button.
    +Click the **Latest Values** button on the **Select widget type** popup.
    +The **Widget Editor** will be opened, pre-populated with the content of default **Latest Values** template widget. + + - Open **Resources** tab and click "Add" then insert the following link: + +``` +https://bernii.github.io/gauge.js/dist/gauge.min.js +{:copy-code} +``` + + - Clear content of the CSS tab of "Resources" section. + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html + +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript +var canvasElement; +var gauge; + +self.onInit = function() { + canvasElement = $('#my-gauge', self.ctx.$container)[0]; + gauge = new Gauge(canvasElement); + gauge.minValue = -1000; + gauge.maxValue = 1000; + gauge.animationSpeed = 16; + self.onResize(); +} + +self.onResize = function() { + canvasElement.width = self.ctx.width; + canvasElement.height = self.ctx.height; + gauge.update(true); + gauge.render(); +} + +self.onDataUpdated = function() { + if (self.ctx.defaultSubscription.data[0].data.length) { + var value = self.ctx.defaultSubscription.data[0].data[0][1]; + gauge.set(value); + } +} +{:copy-code} +``` + + - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/external-js-widget-sample.png) + +In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section. + +The value displayed was obtained from **data** property for the first dataKey. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md new file mode 100644 index 0000000000..6d66e15d4f --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md @@ -0,0 +1,112 @@ +#### Time-Series widget Example with Chart.js library + +
    +
    + +In this example, **Time-Series** line chart widget will be created using external [Chart.js{:target="_blank"}](https://www.chartjs.org/) library. + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button.
    +Click the **Time-Series** button on the **Select widget type** popup.
    +The **Widget Editor** will be opened, pre-populated with the content of default **Time-Series** template widget. + + - Open **Resources** tab and click "Add" then insert the following link: + +``` +https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js +{:copy-code} +``` + + - Clear content of the CSS tab of "Resources" section. + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html + +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript +var myChart; + +self.onInit = function() { + + var chartData = { + datasets: [] + }; + + for (var i=0; i < self.ctx.data.length; i++) { + var dataKey = self.ctx.data[i].dataKey; + var dataset = { + label: dataKey.label, + data: [], + borderColor: dataKey.color, + fill: false + }; + chartData.datasets.push(dataset); + } + + var options = { + maintainAspectRatio: false, + legend: { + display: false + }, + scales: { + xAxes: [{ + type: 'time', + ticks: { + maxRotation: 0, + autoSkipPadding: 30 + } + }] + } + }; + + var canvasElement = $('#myChart', self.ctx.$container)[0]; + var canvasCtx = canvasElement.getContext('2d'); + myChart = new Chart(canvasCtx, { + type: 'line', + data: chartData, + options: options + }); + self.onResize(); +} + +self.onResize = function() { + myChart.resize(); +} + +self.onDataUpdated = function() { + for (var i = 0; i < self.ctx.data.length; i++) { + var datasourceData = self.ctx.data[i]; + var dataSet = datasourceData.data; + myChart.data.datasets[i].data.length = 0; + var data = myChart.data.datasets[i].data; + for (var d = 0; d < dataSet.length; d++) { + var tsValuePair = dataSet[d]; + var ts = tsValuePair[0]; + var value = tsValuePair[1]; + data.push({t: ts, y: value}); + } + } + myChart.options.scales.xAxes[0].ticks.min = self.ctx.timeWindow.minTime; + myChart.options.scales.xAxes[0].ticks.max = self.ctx.timeWindow.maxTime; + myChart.update(); +} +{:copy-code} +``` + + - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png) + +In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section. + +Initially chart datasets prepared using configured dataKeys from **data** property of **ctx**. + +In the **onDataUpdated** function datasources data converted to Chart.js line chart format and pushed to chart datasets. + +Please note that xAxis (time axis) is limited to current timewindow bounds obtained from **timeWindow** property of **ctx**. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md new file mode 100644 index 0000000000..afe4f6a0e1 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md @@ -0,0 +1,47 @@ +#### Sample Latest Values widget + +
    +
    + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.
    +Click the **Latest Values** button on the **Select widget type** popup.
    +The **Widget Editor** will open, pre-populated with the content of the default **Latest Values** template widget. + + - Clear content of the CSS tab of "Resources" section. + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html +
    +
    My first latest values widget.
    +
    +
    {{dataKeyData.dataKey.label}}:
    +
    {{(dataKeyData.data[0] && dataKeyData.data[0][0]) | date : 'yyyy-MM-dd HH:mm:ss' }}
    +
    {{dataKeyData.data[0] && dataKeyData.data[0][1]}}
    +
    +
    +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript + self.onInit = function() { + self.ctx.$scope.data = self.ctx.defaultSubscription.data; + } + + self.onDataUpdated = function() { + self.ctx.detectChanges(); + } +{:copy-code} +``` + + - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/latest-values-widget-sample.png) + +In this example, the **data** property of is assigned to the **$scope** and becomes accessible within the HTML template. + +Inside the HTML, a special [***ngFor**{:target="_blank"}](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available dataKeys & datapoints then render latest values with their corresponding timestamps. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md new file mode 100644 index 0000000000..74bcca2cd5 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md @@ -0,0 +1,187 @@ +#### Sample RPC (Control) widget + +
    +
    + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.
    +Click the **Control Widget** button on the **Select widget type** popup.
    +The **Widget Editor** will open, pre-populated with default **Control** template widget content. + + - Clear content of the CSS tab of "Resources" section. + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html +
    +
    + + RPC method + + + RPC method name is required. + + + + RPC params + + + RPC params is required. + + + +
    + +
    +
    +
    +
    +
    +{:copy-code} +``` + + - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + +```json +{ + "schema": { + "type": "object", + "title": "Settings", + "properties": { + "oneWayElseTwoWay": { + "title": "Is One Way Command", + "type": "boolean", + "default": true + }, + "requestTimeout": { + "title": "RPC request timeout", + "type": "number", + "default": 500 + } + }, + "required": [] + }, + "form": [ + "oneWayElseTwoWay", + "requestTimeout" + ] +} +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript +self.onInit = function() { + + self.ctx.$scope.sendCommand = function() { + var rpcMethod = self.ctx.$scope.rpcMethod; + var rpcParams = self.ctx.$scope.rpcParams; + var timeout = self.ctx.settings.requestTimeout; + var oneWayElseTwoWay = self.ctx.settings.oneWayElseTwoWay ? true : false; + + var commandObservable; + if (oneWayElseTwoWay) { + commandObservable = self.ctx.controlApi.sendOneWayCommand(rpcMethod, rpcParams, timeout); + } else { + commandObservable = self.ctx.controlApi.sendTwoWayCommand(rpcMethod, rpcParams, timeout); + } + commandObservable.subscribe( + function (response) { + if (oneWayElseTwoWay) { + self.ctx.$scope.rpcCommandResponse = "Command was successfully received by device.
    No response body because of one way command mode."; + } else { + self.ctx.$scope.rpcCommandResponse = "Response from device:
    "; + self.ctx.$scope.rpcCommandResponse += JSON.stringify(response, undefined, 2); + } + self.ctx.detectChanges(); + }, + function (rejection) { + self.ctx.$scope.rpcCommandResponse = "Failed to send command to the device:
    " + self.ctx.$scope.rpcCommandResponse += "Status: " + rejection.status + "
    "; + self.ctx.$scope.rpcCommandResponse += "Status text: '" + rejection.statusText + "'"; + self.ctx.detectChanges(); + } + + ); + } + +} +{:copy-code} +``` + + - Fill **Widget title** field with widget type name, for ex. "My first control widget". + - Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section. + - Click dashboard edit button on the preview section to change the size of the resulting widget. Then click dashboard apply button. The final widget should look like the image below. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample.png) + +- Click the **Save** button on the **Widget Editor Toolbar** to save widget type. + +To test how this widget performs RPC commands, we will need to place it in a dashboard then bind it to a device working with RPC commands. To do this, perform the following steps: + +- Login as Tenant administrator. +- Navigate to **Devices** and create new device with some name, for ex. "My RPC Device". +- Open device details and click "Copy Access Token" button to copy device access token to clipboard. +- Download [mqtt-js-rpc-from-server.sh{:target="_blank"}](${siteBaseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.sh) and [mqtt-js-rpc-from-server.js{:target="_blank"}](${siteBaseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.js). Place these files in a folder. + Edit **mqtt-js-rpc-from-server.sh** - replace **$ACCESS_TOKEN** with your device access token from the clipboard. And install mqtt client library. +- Run **mqtt-js-rpc-from-server.sh** script. You should see a "connected" message in the console. +- Navigate to **Dashboards** and create a new dashboard with some name, for ex. "My first control dashboard". Open this dashboard. +- Click dashboard "edit" button. In the dashboard edit mode, click the "Entity aliases" button located on the dashboard toolbar. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png) + +- Inside **Entity aliases** popup click "Add alias". +- Fill "Alias name" field, for ex. "My RPC Device Alias". +- Select "Entity list" in "Filter type" field. +- Choose "Device" in "Type" field. +- Select your device in "Entity list" field. In this example "My RPC Device". + +![image](${helpBaseUrl}/help/images/widget/editor/examples/add-rpc-device-alias.png) + +- Click "Add" and then "Save" in **Entity aliases**. +- Click dashboard "+" button then click "Create new widget" button. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/dashboard-create-new-widget-button.png) + +- Then select **Widget Bundle** where your RPC widget was saved. Select "Control widget" tab. +- Click your widget. In this example, "My first control widget". +- From **Add Widget** popup, select your device alias in **Target device** section. In this example "My RPC Device Alias". +- Click **Add**. Your Control widget will appear in the dashboard. Click dashboard **Apply changes** button to save dashboard and leave editing mode. +- Fill **RPC method** field with RPC method name. For ex. "TestMethod". +- Fill **RPC params** field with RPC params. For ex. "{ param1: "value1" }". +- Click **Send RPC command** button. You should see the following response in the widget. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-response-one-way.png) + +The following output should be printed in the device console: + +```bash + request.topic: v1/devices/me/rpc/request/0 + request.body: {"method":"TestMethod","params":"{ param1: \"value1\" }"} +``` + +In order to test "Two way" RPC command mode, we need to change the corresponding widget settings property. To do this, perform the following steps: + +- Click dashboard "edit" button. In dashboard edit mode, click **Edit widget** button located in the header of Control widget. +- In the widget details, view select "Advanced" tab and uncheck "Is One Way Command" checkbox. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-settings.png) + +- Click **Apply changes** button on the widget details header. Close details and click dashboard **Apply changes** button. +- Fill widget fields with RPC method name and params like in previous steps. + Click **Send RPC command** button. You should see the following response in the widget. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-response-two-way.png) + +- stop **mqtt-js-rpc-from-server.sh** script. + Click **Send RPC command** button. You should see the following response in the widget. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-response-timeout.png) + +In this example, **controlApi** is used to send RPC commands. Additionally, custom widget settings were introduced in order to configure RPC command mode and RPC request timeout. + +The response from the device is handled by **commandObservable**. It has success and failed callbacks with corresponding response, or rejection objects containing information about request execution result. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md new file mode 100644 index 0000000000..7e73b63307 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md @@ -0,0 +1,72 @@ +#### Sample Static widget + +
    +
    + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen and then click the “Create new widget type” button.
    +Click the **Static Widget** button on the **Select widget type** popup.
    +The **Widget Editor** will be opened pre-populated with the content of default **Static** template widget. + + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html +
    +

    My first static widget.

    + +
    +{:copy-code} +``` + + - Put the following JSON content inside the "Settings schema" tab of **Settings schema section**: + +```json +{ + "schema": { + "type": "object", + "title": "Settings", + "properties": { + "alertContent": { + "title": "Alert content", + "type": "string", + "default": "Content derived from alertContent property of widget settings." + } + } + }, + "form": [ + "alertContent" + ] +} +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript +self.onInit = function() { + + self.ctx.$scope.showAlert = function() { + var alertContent = self.ctx.settings.alertContent; + if (!alertContent) { + alertContent = "Content derived from alertContent property of widget settings."; + } + window.alert(alertContent); + }; + +} +{:copy-code} +``` + + - Click the **Run** button on the **Widget Editor Toolbar** to see the resulting **Widget preview** section. + +![image](${helpBaseUrl}/help/images/widget/editor/examples/static-widget-sample.png) + +This is just a static HTML widget. There is no subscription data and no special widget API was used. + +Only custom **showAlert** function was implemented showing an alert with the content of **alertContent** property of widget settings. + +You can switch to dashboard edit mode in **Widget preview** section and change value of **alertContent** by changing widget settings in the "Advanced" tab of widget details. + +Then you can see that the new alert content is displayed. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md b/ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md new file mode 100644 index 0000000000..66cb83b7fe --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md @@ -0,0 +1,93 @@ +#### Sample Time-Series widget + +
    +
    + +In the **Widgets Bundle** view, click the big “+” button at the bottom-right part of the screen, then click the “Create new widget type” button.
    +Click the **Time-Series** button on the **Select widget type** popup.
    +The **Widget Editor** will open, pre-populated with default **Time-Series** template widget content. + + - Replace content of the CSS tab in "Resources" section with the following one: + +```css +.my-data-table th { + text-align: left; +} +{:copy-code} +``` + + - Put the following HTML code inside the HTML tab of "Resources" section: + +```html + + + + + + + + + + + + + + + +
    Timestamp{{dataKeyData.dataKey.label}}
    {{data[0] | date : 'yyyy-MM-dd HH:mm:ss'}}{{dataKeyData.data[$dataIndex] && dataKeyData.data[$dataIndex][1]}}
    +
    +
    +{:copy-code} +``` + + - Put the following JavaScript code inside the "JavaScript" section: + +```javascript +self.onInit = function() { + self.ctx.widgetTitle = 'My first Time-Series widget'; + self.ctx.$scope.datasources = self.ctx.defaultSubscription.datasources; + self.ctx.$scope.data = self.ctx.defaultSubscription.data; + + self.ctx.$scope.datasourceData = []; + + var currentDatasource = null; + var currentDatasourceIndex = -1; + + for (var i=0;i **datasources** and **data** properties are assigned to **$scope** and become accessible within the HTML template. + +The **$scope.datasourceData** property is introduced to map datasource specific dataKeys data by datasource index for flexible access within the HTML template. + +Inside the HTML, a special [***ngFor**{:target="_blank"}](https://angular.io/api/common/NgForOf) structural angular directive is used in order to iterate over available datasources and render corresponding tabs. + +Inside each tab, the table is rendered using dataKeys obtained from **datasourceData** scope property accessed by datasource index.
    + +Each table renders columns by iterating over all **dataKeyData** objects and renders all available datapoints by iterating over **data** array of each **dataKeyData** to render timestamps and values. + +Note that in this code, **onDataUpdated** function is implemented with a call to **detectChanges** function necessary to perform new change detection cycle when new data is received. + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_action_sources_object.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_action_sources_object.md new file mode 100644 index 0000000000..16025b6cfd --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_action_sources_object.md @@ -0,0 +1,17 @@ +#### Action sources object + +
    +
    + +Map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) to which user actions can be assigned. It has the following structure: + +```javascript + return { + 'headerButton': { // Action source Id (unique action source identificator) + name: 'widget-action.header-button', // Display name of action source, used in widget settings ('Actions' tab). + value: 'headerButton', // Action source Id + multiple: true // Boolean property indicating if this action source supports multiple action definitions + // (for ex. multiple buttons in one cell, or only one action can by assigned on table row click.) + } + }; +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_existing_code.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_existing_code.md new file mode 100644 index 0000000000..90585077a5 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_existing_code.md @@ -0,0 +1,62 @@ +#### Using existing JavaScript/Typescript code + +
    +
    + +Another approach of creating widgets is to use existing bundled JavaScript/Typescript code. + +In this case, you can create own TypeScript class or Angular component and bundle it into the ThingsBoard UI code. + +In order to make this code accessible within the widget, you need to register corresponding Angular module or inject TypeScript class to a global variable (for ex. window object). + +Some ThingsBoard widgets already use this approach. Take a look at the [widget-component-service.ts{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts#L140) +or [widget-components.module.ts{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts#L50).
    +Here you can find how some bundled classes or components are registered for later use in ThingsBoard widgets. + +For example "Timeseries - Flot" widget (from "Charts" Widgets Bundle) uses [**TbFlot**{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts#L73) TypeScript class which is injected as window property inside **widget-component-service.ts**: + +```typescript +... + +const widgetModulesTasks: Observable[] = []; +... + +widgetModulesTasks.push(from(import('@home/components/widget/lib/flot-widget')).pipe( + tap((mod) => { + (window as any).TbFlot = mod.TbFlot; + })) +); +... + +``` + +Another example is "Timeseries table" widget (from "Cards" Widgets Bundle) that uses Angular component [**tb-timeseries-table-widget**{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts#L107)
    which is registered as dependency of **WidgetComponentsModule** Angular module inside **widget-components.module.ts**. +Thereby this component becomes available for use inside the widget template HTML. + +```typescript +... + +import { TimeseriesTableWidgetComponent } from '@home/components/widget/lib/timeseries-table-widget.component'; + +... + +@NgModule({ + declarations: + [ +... + TimeseriesTableWidgetComponent, +... + ], +... + exports: [ +... + TimeseriesTableWidgetComponent, +... + ], +... +}) +export class WidgetComponentsModule { } +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md new file mode 100644 index 0000000000..f301e40b77 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md @@ -0,0 +1,135 @@ +#### Widget type JavaScript code + +
    +
    + +All widget related JavaScript code according to the [Widget API{:target="_blank"}](${siteBaseUrl}/docs/user-guide/contribution/widgets-development/#basic-widget-api). +The built-in variable **self** is a reference to the widget instance.
    +Each widget function should be defined as a property of the **self** variable. +**self** variable has property **ctx** of type [WidgetContext{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107) - a reference to widget context that has all necessary API and data used by widget instance. + +In order to implement a new widget, the following JavaScript functions should be defined *(Note: each function is optional and can be implemented according to widget specific behaviour):* + +|{:auto} **Function** | **Description** | +|------------------------------------|----------------------------------------------------------------------------------------| +| ``` onInit() ``` | The first function which is called when widget is ready for initialization. Should be used to prepare widget DOM, process widget settings and initial subscription information. | +| ``` onDataUpdated() ``` | Called when the new data is available from the widget subscription. Latest data can be accessed from the object of widget context (**ctx**). | +| ``` onResize() ``` | Called when widget container is resized. Latest width and height can be obtained from widget context (**ctx**). | +| ``` onEditModeChanged() ``` | Called when dashboard editing mode is changed. Latest mode is handled by isEdit property of **ctx**. | +| ``` onMobileModeChanged() ``` | Called when dashboard view width crosses mobile breakpoint. Latest state is handled by isMobile property of **ctx**. | +| ``` onDestroy() ``` | Called when widget element is destroyed. Should be used to cleanup all resources if necessary. | +| ``` getSettingsSchema() ``` | Optional function returning widget settings schema json as alternative to **Settings schema** of settings section. | +| ``` getDataKeySettingsSchema() ``` | Optional function returning particular data key settings schema json as alternative to **Data key settings schema** tab of settings section. | +| ``` typeParameters() ``` | Returns [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) object describing widget datasource parameters. See | | +| ``` actionSources() ``` | Returns map describing available widget action sources ([WidgetActionSource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L121)) used to define user actions. See | + +
    + +##### Creating simple widgets + +The tutorials below show how to create minimal widgets of each type. +In order to minimize the amount of code, the Angular framework will be used, on which ThingsBoard UI is actually based. +By the way, you can always use pure JavaScript or jQuery API in your widget code. + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +##### Integrating existing code to create widget definition + +Below are some examples demonstrating how external JavaScript libraries or existing code can be reused/integrated to create new widgets. + +###### Using external JavaScript library + +
    + +
    +
    + +
    + +
    +
    + +###### Using existing JavaScript/Typescript code + +
    + +
    +
    + +
    + +##### Widget code debugging tips + +The most simple method of debugging is Web console output. +Just place [**console.log(...)**{:target="_blank"}](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) function inside any part of widget JavaScript code. +Then click **Run** button to restart widget code and observe debug information in the Web console. + +Another and most effective method of debugging is to invoke browser debugger. +Put [**debugger;**{:target="_blank"}](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger) statement into the place of widget code you are interested in and then click **Run** button to restart widget code. +Browser debugger (if enabled) will automatically pause code execution at the debugger statement and you will be able to analyze script execution using browser debugging tools. + +
    + +##### Further reading + +For more information read [Widgets Development Guide{:target="_blank"}](${siteBaseUrl}/docs/user-guide/contribution/widgets-development). + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md new file mode 100644 index 0000000000..87988e8682 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md @@ -0,0 +1,113 @@ +#### Subscription object + +
    +
    + +The widget subscription object is instance of [IWidgetSubscription{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/core/api/widget-api.models.ts#L264") and contains all subscription information, including current data, according to the [widget type{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#widget-types). + +Depending on widget type, subscription object provides different data structures. +For [Latest values{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#latest-values) and [Time-series{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#time-series) widget types, it provides the following properties: + +- **datasources** - array of datasources (Array<[Datasource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L279)>) used by this subscription, using the following structure: + +```javascript + datasources = [ + { // datasource + type: 'entity',// type of the datasource. Can be "function" or "entity" + name: 'name', // name of the datasource (in case of "entity" usually Entity name) + aliasName: 'aliasName', // name of the alias used to resolve this particular datasource Entity + entityName: 'entityName', // name of the Entity used as datasource + entityType: 'DEVICE', // datasource Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.) + entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid. + dataKeys: [ // array of keys (Array) (attributes or timeseries) of the entity used to fetch data + { // dataKey + name: 'name', // the name of the particular entity attribute/timeseries + type: 'timeseries', // type of the dataKey. Can be "timeseries", "attribute" or "function" + label: 'Sin', // label of the dataKey. Used as display value (for ex. in the widget legend section) + color: '#ffffff', // color of the key. Can be used by widget to set color of the key data (for ex. lines in line chart or segments in the pie chart). + funcBody: "", // only applicable for datasource with type "function" and "function" key type. Defines body of the function to generate simulated data. + settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section". + }, + //... + ] + }, + //... + ] +``` + +- **data** - array of latest data (Array<[DatasourceData{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L310)>) received in scope of this subscription, using the following structure: + +```javascript + data = [ + { + datasource: {}, // datasource object of this data. See datasource structure above. + dataKey: {}, // dataKey for which the data is held. See dataKey structure above. + data: [ // array of data points + [ // data point + 1498150092317, // unix timestamp of datapoint in milliseconds + 1, // value, can be either string, numeric or boolean + ], + //... + ] + }, + //... + ] +``` + +For [Alarm widget{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#alarm-widget) type it provides the following properties: + +- **alarmSource** - ([Datasource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L279)) information about entity for which alarms are fetched, using the following structure: + +```javascript + alarmSource = { + type: 'entity',// type of the alarm source. Can be "function" or "entity" + name: 'name', // name of the alarm source (in case of "entity" usually Entity name) + aliasName: 'aliasName', // name of the alias used to resolve this particular alarm source Entity + entityName: 'entityName', // name of the Entity used as alarm source + entityType: 'DEVICE', // alarm source Entity type (for ex. "DEVICE", "ASSET", "TENANT", etc.) + entityId: '943b8cd0-576a-11e7-824c-0b1cb331ec92', // entity identificator presented as string uuid. + dataKeys: [ // array of keys indicating alarm fields used to display alarms data + { // dataKey + name: 'name', // the name of the particular alarm field + type: 'alarm', // type of the dataKey. Only "alarm" in this case. + label: 'Severity', // label of the dataKey. Used as display value (for ex. as a column title in the Alarms table) + color: '#ffffff', // color of the key. Can be used by widget to set color of the key data. + settings: {} // dataKey specific settings with structure according to the defined Data key settings json schema. See "Settings schema section". + }, + //... + ] + } +``` + +- **alarms** - array of alarms (Array<[Alarm{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/alarm.models.ts#L89)>) received in scope of this subscription, using the following structure: + +```javascript + alarms = [ + { // alarm + id: { // alarm id + entityType: "ALARM", + id: "943b8cd0-576a-11e7-824c-0b1cb331ec92" + }, + createdTime: 1498150092317, // Alarm created time (unix timestamp) + startTs: 1498150092316, // Alarm started time (unix timestamp) + endTs: 1498563899065, // Alarm end time (unix timestamp) + ackTs: 0, // Time of alarm acknowledgment (unix timestamp) + clearTs: 0, // Time of alarm clear (unix timestamp) + originator: { // Originator - id of entity produced this alarm + entityType: "ASSET", + id: "ceb16a30-4142-11e7-8b30-d5d66714ea5a" + }, + originatorName: "Originator Name", // Name of originator entity + type: "Temperature", // Type of the alarm + severity: "CRITICAL", // Severity of the alarm ("CRITICAL", "MAJOR", "MINOR", "WARNING", "INDETERMINATE") + status: "ACTIVE_UNACK", // Status of the alarm + // ("ACTIVE_UNACK" - active unacknowledged, + // "ACTIVE_ACK" - active acknowledged, + // "CLEARED_UNACK" - cleared unacknowledged, + // "CLEARED_ACK" - cleared acknowledged) + details: {} // Alarm details object derived from alarm details json. + } + ] +``` + +For [RPC{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#rpc-control-widget) or [Static{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#static) widget types, subscription object is optional and does not contain necessary information. diff --git a/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_type_parameters_object.md b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_type_parameters_object.md new file mode 100644 index 0000000000..09ff8ccb93 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/editor/widget_js_type_parameters_object.md @@ -0,0 +1,14 @@ +#### Type parameters object + +
    +
    + +Object [WidgetTypeParameters{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L151) describing widget datasource parameters. It has the following properties: + +```javascript + return { + maxDatasources: -1, // Maximum allowed datasources for this widget, -1 - unlimited + maxDataKeys: -1, //Maximum allowed data keys for this widget, -1 - unlimited + dataKeysOptional: false //Whether this widget can be configured with datasources without data keys + } +``` diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/alarm/cell_content_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/alarm/cell_content_fn.md new file mode 100644 index 0000000000..e757bed970 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/alarm/cell_content_fn.md @@ -0,0 +1,52 @@ +#### Cell content function + +
    +
    + +*function (value, alarm, ctx): string* + +A JavaScript function used to compute alarm cell content HTML depending on alarm field value. + +**Parameters:** + +
      +
    • value: any - An alarm field value displayed in the cell. +
    • +
    • alarm: AlarmDataInfo - An + AlarmDataInfo object + presenting basic alarm properties (ex. type, severity, originator, etc.) and
      provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return string value presenting cell content HTML. + +
    + +##### Examples + +* Format alarm start time using date/time pattern: + +```javascript +var startTime = value; +return startTime ? ctx.date.transform(startTime, 'yyyy-MM-dd HH:mm:ss') : ''; +{:copy-code} +``` + +* Styled cell content for originator alarm field: + +```javascript +var originator = value; +return '
    ' + originator + '
    '; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/alarm/cell_style_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/alarm/cell_style_fn.md new file mode 100644 index 0000000000..2c6ab6da20 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/alarm/cell_style_fn.md @@ -0,0 +1,62 @@ +#### Cell style function + +
    +
    + +*function (value, alarm, ctx): {[key: string]: string}* + +A JavaScript function used to compute alarm cell style depending on alarm field value. + +**Parameters:** + +
      +
    • value: any - An alarm field value displayed in the cell. +
    • +
    • alarm: AlarmDataInfo - An + AlarmDataInfo object + presenting basic alarm properties (ex. type, severity, originator, etc.) and
      provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return key/value object presenting style attributes. + +
    + +##### Examples + +* Set color depending on alarm severity: + +```javascript +var severity = value; +var color = 'black'; +switch (severity) { + case 'CRITICAL': + color = 'red'; + break; + case 'MAJOR': + color = 'orange'; + break; + case 'MINOR': + color = '#ffca3d'; + break; + case 'WARNING': + color = '#abab00'; + break; + case 'INDETERMINATE': + color = 'green'; + break; +} +return { + fontWeight: 'bold', + color: color +}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/alarm/row_style_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/alarm/row_style_fn.md new file mode 100644 index 0000000000..e65a72fc0d --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/alarm/row_style_fn.md @@ -0,0 +1,59 @@ +#### Row style function + +
    +
    + +*function (alarm, ctx): {[key: string]: string}* + +A JavaScript function used to compute alarm row style depending on alarm value. + +**Parameters:** + +
      +
    • alarm: AlarmDataInfo - An + AlarmDataInfo object + presenting basic alarm properties (ex. type, severity, originator, etc.) and
      provides access to other alarm or originator entity fields/attributes/timeseries declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return key/value object presenting style attributes. + +
    + +##### Examples + +* Set row background color depending on alarm severity: + +```javascript +var severity = alarm.severity; +var color = '#fff'; +switch (severity) { + case 'CRITICAL': + color = 'red'; + break; + case 'MAJOR': + color = 'orange'; + break; + case 'MINOR': + color = '#ffca3d'; + break; + case 'WARNING': + color = '#abab00'; + break; + case 'INDETERMINATE': + color = 'green'; + break; +} +return { + backgroundColor: color +}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md new file mode 100644 index 0000000000..b532cfa3f8 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md @@ -0,0 +1,40 @@ +#### Node disabled function + +
    +
    + +*function (nodeCtx): boolean* + +A JavaScript function evaluating whether current node should be disabled (not selectable). + +**Parameters:** + +
      +
    • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +`true` if node should be disabled (not selectable), `false` otherwise. + +
    + +##### Examples + +* Disable current node according to the value of example `nodeDisabled` attribute: + +```javascript +var data = nodeCtx.data; +if (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) { + return data['nodeDisabled'] === 'true'; +} else { + return false; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md new file mode 100644 index 0000000000..1a71858c9b --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md @@ -0,0 +1,47 @@ +#### Node has children function + +
    +
    + +*function (nodeCtx): boolean* + +A JavaScript function evaluating whether current node has children (whether it can be expanded). + +**Parameters:** + +
      +
    • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +`true` if node should have children, `false` otherwise. + +
    + +##### Examples + +* Restrict entities hierarchy expansion up to third level: + +```javascript +return nodeCtx.level <= 2; +{:copy-code} +``` + +* Restrict entities expansion according to the value of example `nodeHasChildren` attribute: + +```javascript +var data = nodeCtx.data; +if (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) { + return data['nodeHasChildren'] === 'true'; +} else { + return true; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md new file mode 100644 index 0000000000..1a3c0aec8e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md @@ -0,0 +1,53 @@ +#### Node icon function + +
    +
    + +*function (nodeCtx): {iconUrl?: string, materialIcon?: string} | 'default'* + +A JavaScript function used to compute node icon info. + +**Parameters:** + +
      +
    • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +Should return node icon info object with the following structure: + +```typescript +{ + iconUrl?: string, + materialIcon?: string +} +``` +Resulting object should contain either `materialIcon` or `iconUrl` property.
    +Where: + - `materialIcon` - name of the material icon to be used from the [Material Icons Library{:target="_blank"}](https://material.io/tools/icons); + - `iconUrl` - url of the external image to be used as node icon. + +Function can return `default` string value. In this case default icons according to entity type will be used. + +
    + +##### Examples + +* Use external image for devices which name starts with `Test` and use default icons for the rest of entities: + +```javascript +var entity = nodeCtx.entity; +if (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) { + return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'}; +} else { + return 'default'; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md new file mode 100644 index 0000000000..e85a2b1aa9 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md @@ -0,0 +1,35 @@ +#### Node opened by default function + +
    +
    + +*function (nodeCtx): boolean* + +A JavaScript function evaluating whether current node should be opened (expanded) when it first loaded. + +**Parameters:** + +
      +
    • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +`true` if node should be opened (expanded), `false` otherwise. + +
    + +##### Examples + +* Open by default nodes up to third level: + +```javascript +return nodeCtx.level <= 2; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md new file mode 100644 index 0000000000..f312483d07 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md @@ -0,0 +1,49 @@ +#### Node relations query function + +
    +
    + +*function (nodeCtx): [EntityRelationsQuery{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/dda61383933cac9aa6821a77ff9b19291e69db9f/ui-ngx/src/app/shared/models/relation.models.ts#L69) | 'default'* + +A JavaScript function used to compute child nodes relations query for current node. + +**Parameters:** + +
      +
    • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +Should return [EntityRelationsQuery{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/dda61383933cac9aa6821a77ff9b19291e69db9f/ui-ngx/src/app/shared/models/relation.models.ts#L69) for current node used to fetch entity children.
    +Function can return `default` string value. In this case default relations query will be used. + +
    + +##### Examples + +* Fetch child entities having relations of type `Contains` from the current entity: + +```javascript +var entity = nodeCtx.entity; +var query = { + parameters: { + rootId: entity.id.id, + rootType: entity.id.entityType, + direction: "FROM", + maxLevel: 1 + }, + filters: [{ + relationType: "Contains", + entityTypes: [] + }] +}; +return query; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md new file mode 100644 index 0000000000..76b70a73fb --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md @@ -0,0 +1,41 @@ +#### Node text function + +
    +
    + +*function (nodeCtx): string* + +A JavaScript function used to compute text or HTML code for the current node. + +**Parameters:** + +
      +
    • nodeCtx: HierarchyNodeContext - An + HierarchyNodeContext object + containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +Should return string value presenting text or HTML for the current node. + +
    + +##### Examples + +* Display entity name and optionally temperature value if it is present in entity attributes/timeseries: + +```javascript +var data = nodeCtx.data; +var entity = nodeCtx.entity; +var text = entity.name; +if (data.hasOwnProperty('temperature') && data['temperature'] !== null) { + text += " "+ data['temperature'] +" °C"; +} +return text; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md new file mode 100644 index 0000000000..779424b553 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md @@ -0,0 +1,44 @@ +#### Nodes sort function + +
    +
    + +*function (nodeCtx1, nodeCtx2): number* + +A JavaScript function used to compare nodes of the same level when sorting. + +**Parameters:** + + + +**Returns:** + +Should return integer value presenting nodes comparison result: +- **less than 0** - sort `nodeCtx1` to an index lower than `nodeCtx2`; +- **0** - leave `nodeCtx1` and `nodeCtx2` unchanged with respect to each other; +- **greater than 0** - sort `nodeCtx2` to an index lower than `nodeCtx1`; + +
    + +##### Examples + +* Sort entities first by entity type in alphabetical order then by entity name in alphabetical order: + +```javascript +var result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType); +if (result === 0) { + result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name); +} +return result; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entity/cell_content_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entity/cell_content_fn.md new file mode 100644 index 0000000000..e2dd12e841 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entity/cell_content_fn.md @@ -0,0 +1,143 @@ +#### Cell content function + +
    +
    + +*function (value, entity, ctx): string* + +A JavaScript function used to compute entity cell content HTML depending on entity field value. + +**Parameters:** + +
      +
    • value: any - An entity field value displayed in the cell. +
    • +
    • entity: EntityData - An + EntityData object + presenting basic entity properties (ex. id, entityName) and
      provides access to other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return string value presenting cell content HTML. + +
    + +##### Examples + +* Format entity created time using date/time pattern: + +```javascript +var createdTime = value; +return createdTime ? ctx.date.transform(createdTime, 'yyyy-MM-dd HH:mm:ss') : ''; +{:copy-code} +``` + +* Styled cell content for device type field: + +```javascript +var deviceType = value; +var color = '#fff'; +switch (deviceType) { + case 'thermostat': + color = 'orange'; + break; + case 'default': + color = '#abab00'; + break; +} +return '
    ' + deviceType + '
    '; +{:copy-code} +``` + +* Colored circles instead of boolean value: + +```javascript +var color; +var active = value; +if (active == 'true') { // all key values here are strings + color = '#27AE60'; +} else { + color = '#EB5757'; +} +return ''; +{:copy-code} +``` + +* Decimal value format (1196 => 1,196.0): + +```javascript +var value = value / 1; +function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} +return value ? numberWithCommas(value.toFixed(1)) : ''; +{:copy-code} +``` + +* Show device status and icon this : + +```javascript +{:code-style="max-height: 200px; max-width: 850px;"} +function getIcon(value) { + if (value == 'QUEUED') { + return '' + + '' + + '' + + '' + + ''; + } + if (value == 'UPDATED' ) { + return '' + + 'update' + + ''; + } + return ''; +} +function capitalize (s) { + if (typeof s !== 'string') return ''; + return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(); +} +var status = value; +return getIcon(status) + '' + capitalize(status) + ''; +{:copy-code} +``` + +* Display device attribute value on progress bar: + +```javascript +{:code-style="max-height: 200px; max-width: 850px;"} +var progress = value; +if (value !== '') { + return `` + + `` + + ``; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entity/cell_style_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entity/cell_style_fn.md new file mode 100644 index 0000000000..442fca3375 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entity/cell_style_fn.md @@ -0,0 +1,62 @@ +#### Cell style function + +
    +
    + +*function (value, entity, ctx): {[key: string]: string}* + +A JavaScript function used to compute entity cell style depending on entity field value. + +**Parameters:** + +
      +
    • value: any - An entity field value displayed in the cell. +
    • +
    • entity: EntityData - An + EntityData object + presenting basic entity properties (ex. id, entityName) and
      provides access to other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return key/value object presenting style attributes. + +
    + +##### Examples + +* Set color and font-weight table cell content: + +```javascript +return { + color:'rgb(0, 132, 214)', + fontWeight: 600 +} +{:copy-code} +``` + +* Set color depending on device temperature value: + +```javascript +var temperature = value; +var color = 'black'; +if (temperature) { + if (temperature > 25) { + color = 'red'; + } else { + color = 'green'; + } +} +return { + fontWeight: 'bold', + color: color +}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entity/row_style_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entity/row_style_fn.md new file mode 100644 index 0000000000..4867cb9b38 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entity/row_style_fn.md @@ -0,0 +1,60 @@ +#### Row style function + +
    +
    + +*function (entity, ctx): {[key: string]: string}* + +A JavaScript function used to compute entity row style depending on entity value. + +**Parameters:** + +
      +
    • entity: EntityData - An + EntityData object + presenting basic entity properties (ex. id, entityName) and
      provides access to other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return key/value object presenting style attributes. + +
    + +##### Examples + +* Set color and font-weight table row: + +```javascript +return { + color:'rgb(0, 132, 214)', + fontWeight: 600 +} +{:copy-code} +``` + +* Set row background color depending on device type: + +```javascript +var deviceType = entity.Type; +var color = '#fff'; +switch (deviceType) { + case 'thermostat': + color = 'orange'; + break; + case 'default': + color = '#abab00'; + break; +} +return { + backgroundColor: color +}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/flot/point_shape_format_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/flot/point_shape_format_fn.md new file mode 100644 index 0000000000..c36f7e3cc7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/flot/point_shape_format_fn.md @@ -0,0 +1,111 @@ +#### Point shape draw function + +
    +
    + +*function (ctx, x, y, radius, shadow): void* + +A JavaScript function used to draw custom shapes for chart points when `Custom function` for point shape is selected. + +**Parameters:** + +
      +
    • + ctx: CanvasRenderingContext2D - A canvas drawing context. +
    • +
    • + x number - point center X coordinate. +
    • +
    • + y number - point center Y coordinate. +
    • +
    • + radius number - point radius. +
    • +
    • + shadow boolean - whether to draw shadow. +
    • +
    + +
    + +##### Examples + +* Draw square: + +```javascript +var size = radius * Math.sqrt(Math.PI) / 2; +ctx.rect(x - size, y - size, size + size, size + size); +{:copy-code} +``` + +* Draw circle: + +```javascript +ctx.moveTo(x + radius, y); +ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false); +{:copy-code} +``` + +* Draw diamond: + +```javascript +var size = radius * Math.sqrt(Math.PI / 2); +ctx.moveTo(x - size, y); +ctx.lineTo(x, y - size); +ctx.lineTo(x + size, y); +ctx.lineTo(x, y + size); +ctx.lineTo(x - size, y); +ctx.lineTo(x, y - size); +{:copy-code} +``` + +* Draw triangle: + +```javascript +var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3)); +var height = size * Math.sin(Math.PI / 3); +ctx.moveTo(x - size / 2, y + height / 2); +ctx.lineTo(x + size / 2, y + height / 2); +if (!shadow) { + ctx.lineTo(x, y - height / 2); + ctx.lineTo(x - size / 2, y + height / 2); + ctx.lineTo(x + size / 2, y + height / 2); +} +{:copy-code} +``` + +* Draw cross: + +```javascript +var size = radius * Math.sqrt(Math.PI) / 2; +ctx.moveTo(x - size, y - size); +ctx.lineTo(x + size, y + size); +ctx.moveTo(x - size, y + size); +ctx.lineTo(x + size, y - size); +{:copy-code} +``` + +* Draw ellipse: + +```javascript +if (!shadow) { + ctx.moveTo(x + radius, y); + ctx.arc(x, y, radius, 0, Math.PI * 2, false); +} +{:copy-code} +``` + +* Draw plus: + +```javascript +var size = radius * Math.sqrt(Math.PI / 2); +ctx.moveTo(x - size, y); +ctx.lineTo(x + size, y); +ctx.moveTo(x, y + size); +ctx.lineTo(x, y - size); +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/flot/ticks_formatter_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/flot/ticks_formatter_fn.md new file mode 100644 index 0000000000..12de0f782c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/flot/ticks_formatter_fn.md @@ -0,0 +1,104 @@ +#### Ticks formatter function + +
    +
    + +*function (value): string* + +A JavaScript function used to format Y axis ticks. + +**Parameters:** + +
      +
    • value: number - A tick value that should be formatted. +
    • +
    + +**Returns:** + +A string presenting the formatted value to be displayed as Y axis tick. + +
    + +##### Examples + +* Display ticks as is: + +```javascript +return value; +{:copy-code} +``` + +* Present ticks in Amperage (A) units and two decimal places: + +```javascript +return value.toFixed(2) + ' A'; +{:copy-code} +``` + +* Disable ticks: + +```javascript +return ''; +{:copy-code} +``` + +* Present ticks in decimal format (1196 => 1,196.0): + +```javascript +var value = value / 1; +function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} +return value ? numberWithCommas(value.toFixed(1)) : ''; +{:copy-code} +``` + +
      +
    • +To present axis ticks for true / false or 1 / 0 data.
      +Display On when value > 0 and <= 1,
      +Off when value = 0,
      +disable for all other values.
      +Note: To avoid duplicates among Y axis ticks it is recommended to set Steps size between ticks to 1: +
    • +
    + +```javascript +if (value > 0 && value <= 1) { + return 'On'; +} else if (value === 0) { + return 'Off'; +} else { + return ''; +} +{:copy-code} +``` + +
      +
    • +To present axis ticks for state or level data.
      +Display High when value >= 2,
      +Medium when value >= 1 and < 2,
      +Low when value >= 0 and < 1,
      +disable for all other values.
      +Note: To avoid duplicates among Y axis ticks it is recommended to set Steps size between ticks to 1
      +or other suitable value depending on your case: +
    • +
    + +```javascript +if (value >= 2) { + return 'High'; +} else if (value >= 1) { + return 'Medium'; +} else if (value >= 0) { + return 'Low'; +} else { + return ''; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/flot/tooltip_value_format_fn.md similarity index 71% rename from ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md rename to ui-ngx/src/assets/help/en_US/widget/lib/flot/tooltip_value_format_fn.md index c3451cfe16..bffbfe4673 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/tooltip_value_format_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/flot/tooltip_value_format_fn.md @@ -35,3 +35,17 @@ return value + ' °C'; return value.toFixed(2) + ' A'; {:copy-code} ``` + +* Present the datapoint value in decimal format (1196 => 1,196.0): + +```javascript +var value = value / 1; +function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} +return value ? numberWithCommas(value.toFixed(1)) : ''; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md new file mode 100644 index 0000000000..64e994fc3e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md @@ -0,0 +1,42 @@ +#### Marker color function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the marker. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting color of the marker. + +In case no data is returned, color value from **Color** settings field will be used. + +
    + +##### Examples + +* Calculate color depending on `temperature` telemetry value for `colorpin` device type: + +```javascript +var type = data['Type']; +if (type == 'colorpin') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', amount = percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md new file mode 100644 index 0000000000..eb3e6f981e --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/label_fn.md @@ -0,0 +1,40 @@ +#### Marker label function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute text or HTML code of the marker label. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML of the marker label. + +
    + +##### Examples + +* Display styled label with corresponding latest telemetry data for `energy meter` or `thermometer` device types: + +```javascript +var deviceType = data['Type']; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}, ${energy:2} kWt'; + } else if (deviceType == "thermometer") { + return '${entityName}, ${temperature:2} °C'; + } +} +return data.entityName; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md new file mode 100644 index 0000000000..7a4f24f0ac --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/map_fn_args.md @@ -0,0 +1,10 @@ +
  • data: FormattedData - A FormattedData object associated with marker or data point of the route.
    + Represents basic entity properties (ex. entityId, entityName)
    and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
  • +
  • dsData: FormattedData[] - All available data associated with markers or routes data points as array of FormattedData objects
    + resolved from configured datasources. Each object represents basic entity properties (ex. entityId, entityName)
    + and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
  • +
  • dsIndex number - index of the current marker data or route data point in dsData array.
    + Note: The data argument is equivalent to dsData[dsIndex] expression. +
  • diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn.md new file mode 100644 index 0000000000..565de0147a --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/marker_image_fn.md @@ -0,0 +1,62 @@ +#### Marker image function + +
    +
    + +*function (data, images, dsData, dsIndex): {url: string, size: number}* + +A JavaScript function used to compute marker image. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return marker image data having the following structure: + +```typescript +{ + url: string, + size: number +} +``` + +- *url* - marker image url; +- *size* - marker image size; + +In case no data is returned, default marker image will be used. + +
    + +##### Examples + +
      +
    • +Calculate image url depending on temperature telemetry value for thermometer device type.
      +Let's assume 4 images are defined in Marker images section. Each image corresponds to particular temperature level: +
    • +
    + +```javascript +var type = data['Type']; +if (type == 'thermometer') { + var res = { + url: images[0], + size: 40 + } + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120; + var index = Math.min(3, Math.floor(4 * percent)); + res.url = images[index]; + } + return res; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md new file mode 100644 index 0000000000..056e47d0eb --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md @@ -0,0 +1,42 @@ +#### Path color function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the trip path. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting color of the trip path. + +In case no data is returned, color value from **Path color** settings field will be used. + +
    + +##### Examples + +* Calculate color depending on `temperature` telemetry value for `colorpin` device type: + +```javascript +var type = data['Type']; +if (type == 'colorpin') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', amount = percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md new file mode 100644 index 0000000000..30f5e0f4d5 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md @@ -0,0 +1,43 @@ +#### Path point color function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the trip path point. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting color of the trip path point. + +In case no data is returned, color value from **Point color** settings field will be used. + +
    + +##### Examples + +* Calculate color depending on `temperature` telemetry value for `colorpin` device type: + +```javascript +var type = data['Type']; +if (type == 'colorpin') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', amount = percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
    +
    + diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md new file mode 100644 index 0000000000..736bd727f0 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md @@ -0,0 +1,42 @@ +#### Polygon color function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute color of the polygon. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting color of the polygon. + +In case no data is returned, color value from **Polygon color** settings field will be used. + +
    + +##### Examples + +* Calculate color depending on `temperature` telemetry value for `thermostat` device type: + +```javascript +var type = data['Type']; +if (type == 'thermostat') { + var temperature = data['temperature']; + if (typeof temperature !== undefined) { + var percent = (temperature + 60)/120 * 100; + return tinycolor.mix('blue', 'red', amount = percent).toHexString(); + } + return 'blue'; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md new file mode 100644 index 0000000000..86de41c185 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_tooltip_fn.md @@ -0,0 +1,40 @@ +#### Polygon tooltip function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute text or HTML code to be displayed in the polygon tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the polygon tooltip. + +
    + +##### Examples + +* Display details with corresponding telemetry data for `energy meter` or `thermostat` device types: + +```javascript +var deviceType = data['Type']; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}
    Energy: ${energy:2} kWt
    '; + } else if (deviceType == "thermostat") { + return '${entityName}
    Temperature: ${temperature:2} °C
    '; + } +} +return data.entityName; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md new file mode 100644 index 0000000000..98821e70a5 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/position_fn.md @@ -0,0 +1,41 @@ +#### Position conversion function + +
    +
    + +*function (origXPos, origYPos): {x: number, y: number}* + +A JavaScript function used to convert original relative x, y coordinates of the marker. + +**Parameters:** + +- **origXPos:** number - original relative x coordinate as double from 0 to 1; +- **origYPos:** number - original relative y coordinate as double from 0 to 1; + +**Returns:** + +Should return position data having the following structure: + +```typescript +{ + x: number, + y: number +} +``` + +- *x* - new relative x coordinate as double from 0 to 1; +- *y* - new relative y coordinate as double from 0 to 1; + +
    + +##### Examples + +* Scale the coordinates to half the original: + +```javascript +return {x: origXPos / 2, y: origYPos / 2}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md new file mode 100644 index 0000000000..d4d97d99b6 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/tooltip_fn.md @@ -0,0 +1,40 @@ +#### Marker tooltip function + +
    +
    + +*function (data, dsData, dsIndex): string* + +A JavaScript function used to compute text or HTML code to be displayed in the marker, point or polygon tooltip. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +Should return string value presenting text or HTML for the tooltip. + +
    + +##### Examples + +* Display details with corresponding telemetry data for `thermostat` device type: + +```javascript +var deviceType = data['Type']; +if (typeof deviceType !== undefined) { + if (deviceType == "energy meter") { + return '${entityName}
    Energy: ${energy:2} kWt
    '; + } else if (deviceType == "thermometer") { + return '${entityName}
    Temperature: ${temperature:2} °C
    '; + } +} +return data.entityName; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md new file mode 100644 index 0000000000..e938532b78 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/map/trip_point_as_anchor_fn.md @@ -0,0 +1,34 @@ +#### Point as anchor function + +
    +
    + +*function (data, dsData, dsIndex): boolean* + +A JavaScript function evaluating whether to use trip point as time anchor used in time selector. + +**Parameters:** + +
      + {% include widget/lib/map/map_fn_args %} +
    + +**Returns:** + +`true` if the point should be decided as anchor, `false` otherwise. + +In case no data is returned, the point is not used as anchor. + +
    + +##### Examples + +* Make anchors with 5 seconds step interval: + +```javascript +return data.time % 5000 < 1000; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/markdown/markdown_text_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/markdown/markdown_text_fn.md new file mode 100644 index 0000000000..9bbcb7fd7c --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/markdown/markdown_text_fn.md @@ -0,0 +1,58 @@ +#### Markdown text function + +
    +
    + +*function (data): string* + +A JavaScript function used to calculate markdown or HTML content. + +**Parameters:** + +
      +
    • data: FormattedData[] - An array of FormattedData objects resolved from configured datasources.
      + Each object represents basic entity properties (ex. entityId, entityName)
      and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +Should return string value presenting markdown or HTML content. + +
    + +##### Examples + +* Display markdown with first entity name information: + +```javascript +return '# Some title\n - Entity name: ' + data[0]['entityName']; +{:copy-code} +``` + +
      +
    • +Display greetings for currently logged-in user.
      +Let's assume widget has first datasource configured using Current User Single entity alias
      +and has data keys for firstName, lastName and name entity fields: +
    • +
    + +```javascript +var userEntity = data[0]; +var userName; +if (userEntity.firstName || userEntity['First name']) { + userName = userEntity.firstName || userEntity['First name']; +} else if (userEntity.lastName || userEntity['Last name']) { + userName = userEntity.lastName || userEntity['Last name']; +} else if (userEntity.name || userEntity['Name']) { + userName = userEntity.name || userEntity['Name']; +} + +var welcomeText = 'Hi, ' + userName + '!\n\n'; +return welcomeText; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/qrcode/qrcode_text_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/qrcode/qrcode_text_fn.md new file mode 100644 index 0000000000..2d7f40bf00 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/qrcode/qrcode_text_fn.md @@ -0,0 +1,55 @@ +#### QR code text function + +
    +
    + +*function (data): string* + +A JavaScript function used to calculate text to be displayed as QR code. + +**Parameters:** + +
      +
    • data: FormattedData[] - An array of FormattedData objects resolved from configured datasources.
      + Each object represents basic entity properties (ex. entityId, entityName)
      and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
    • +
    + +**Returns:** + +Should return string value presenting text to be displayed as QR code. + +
    + +##### Examples + +* Prepare QR code text from name of the first entity if present: + +```javascript +return data[0] ? data[0]['entityName'] : ''; +{:copy-code} +``` + +
      +
    • +Prepare QR code text to use as device claiming info (in this case {deviceName: string, secretKey: string}).
      +Let's assume device has claimingData attribute with string JSON value containing secretKey field
      +(see Claiming devices): +
    • +
    + +```javascript +var entityData = data[0]; +if (entityData) { + return JSON.stringify({ + deviceName: entityData.entityName, + secretKey: JSON.parse(entityData.claimingData).secretKey + }); +} else { + return ''; +} +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/rpc/convert_value_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/rpc/convert_value_fn.md new file mode 100644 index 0000000000..816fa10263 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/rpc/convert_value_fn.md @@ -0,0 +1,40 @@ +#### Convert value function + +
    +
    + +*function (value): any* + +A JavaScript function converting target on/off state of the control widget to payload of the RPC set value command. + +**Parameters:** + +
      +
    • value: boolean - value indicating target on/off state of the control widget. +
    • +
    + +**Returns:** + +Payload object or primitive to be used by the RPC set value command. + +
    + +##### Examples + +* Use original target value as payload: + +```javascript +return value; +{:copy-code} +``` + +* Create json payload with `enabled` boolean property: + +```javascript +return { enabled: value }; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/rpc/parse_gpio_status_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/rpc/parse_gpio_status_fn.md new file mode 100644 index 0000000000..417283a579 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/rpc/parse_gpio_status_fn.md @@ -0,0 +1,35 @@ +#### Parse GPIO status function + +
    +
    + +*function (body, pin): boolean* + +A JavaScript function evaluating enabled/disable state of GPIO pin from the response of GPIO status request. + +**Parameters:** + +
      +
    • body: any - response body of the GPIO status request. +
    • +
    • pin: number - number of the GPIO pin. +
    • +
    + +**Returns:** + +`true` if GPIO pin should be enabled, `false` otherwise. + +
    + +##### Examples + +* Detect status of the pin assuming response body is array of boolean pins states: + +```javascript +return body[pin] === true; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/rpc/parse_value_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/rpc/parse_value_fn.md new file mode 100644 index 0000000000..c2479de798 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/rpc/parse_value_fn.md @@ -0,0 +1,41 @@ +#### Parse value function + +
    +
    + +*function (data): boolean* + +A JavaScript function converting attribute/timeseries value or value of the RPC command response to boolean value. + +**Parameters:** + +
      +
    • data: any - attribute/timeseries value or value of the RPC command response. +
    • +
    + +**Returns:** + +`true` if control widget should be switched on, `false` otherwise. + +
    + +##### Examples + +* Switch on control widget for any positive value: + +```javascript +return data ? true : false; +{:copy-code} +``` + +* Parse control widget state from json payload having `enabled` boolean property: + +```javascript +var payload = typeof data === 'string' ? JSON.parse(data) : data; +return payload && payload.enabled ? true : false; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/cell_content_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/cell_content_fn.md new file mode 100644 index 0000000000..0fe3b8b4e3 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/cell_content_fn.md @@ -0,0 +1,53 @@ +#### Cell content function + +
    +
    + +*function (value, rowData, ctx): string* + +A JavaScript function used to compute timeseries cell content HTML depending on timeseries field value. + +**Parameters:** + +
      +
    • value: any - An entity field value displayed in the cell. +
    • +
    • rowData: TimeseriesRow - A + TimeseriesRow object + presenting formattedTs (a string value of formatted timestamp) and
      timeseries values for each column declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return string value presenting cell content HTML. + +
    + +##### Examples + +* Styled cell content for temperature field: + +```javascript +var temperature = value; +var color = '#fff'; +if (temperature) { + if (temperature > 25) { + color = 'red'; + } else { + color = 'green'; + } +} +return '
    ' + temperature + '
    '; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/cell_style_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/cell_style_fn.md new file mode 100644 index 0000000000..202423fbb7 --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/cell_style_fn.md @@ -0,0 +1,52 @@ +#### Cell style function + +
    +
    + +*function (value, rowData, ctx): {[key: string]: string}* + +A JavaScript function used to compute timeseries cell style depending on timeseries field value. + +**Parameters:** + +
      +
    • value: any - An timeseries field value displayed in the cell. +
    • +
    • rowData: TimeseriesRow - A + TimeseriesRow object + presenting formattedTs (a string value of formatted timestamp) and
      timeseries values for each column declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return key/value object presenting style attributes. + +
    + +##### Examples + +* Set color depending on temperature value: + +```javascript +var temperature = value; +var color = 'black'; +if (temperature) { + if (temperature > 25) { + color = 'red'; + } else { + color = 'green'; + } +} +return { + fontWeight: 'bold', + color: color +}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/row_style_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/row_style_fn.md new file mode 100644 index 0000000000..fd1ea7ce7d --- /dev/null +++ b/ui-ngx/src/assets/help/en_US/widget/lib/timeseries/row_style_fn.md @@ -0,0 +1,49 @@ +#### Row style function + +
    +
    + +*function (rowData, ctx): {[key: string]: string}* + +A JavaScript function used to compute timeseries row style depending on row value. + +**Parameters:** + +
      +
    • rowData: TimeseriesRow - A + TimeseriesRow object + presenting formattedTs (a string value of formatted timestamp) and
      timeseries values for each column declared in widget datasource configuration. +
    • +
    • ctx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • +
    + +**Returns:** + +Should return key/value object presenting style attributes. + +
    + +##### Examples + +* Set row background color depending on temperature value: + +```javascript +var temperature = rowData.temperature; +var color = '#fff'; +if (temperature) { + if (temperature > 25) { + color = 'red'; + } else { + color = 'green'; + } +} +return { + backgroundColor: color +}; +{:copy-code} +``` + +
    +
    diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/filter-node.png b/ui-ngx/src/assets/help/images/rulenode/examples/filter-node.png new file mode 100644 index 0000000000..178df7e314 Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/filter-node.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/switch-node.png b/ui-ngx/src/assets/help/images/rulenode/examples/switch-node.png new file mode 100644 index 0000000000..88739a72e5 Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/switch-node.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/add-rpc-device-alias.png b/ui-ngx/src/assets/help/images/widget/editor/examples/add-rpc-device-alias.png new file mode 100644 index 0000000000..6d7e3c22ac Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/add-rpc-device-alias.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/alarm-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/alarm-widget-sample.png new file mode 100644 index 0000000000..8d9b4f3dfb Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/alarm-widget-sample.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-one-way.png b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-one-way.png new file mode 100644 index 0000000000..579e4edbc5 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-one-way.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-timeout.png b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-timeout.png new file mode 100644 index 0000000000..2a71e15795 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-timeout.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-two-way.png b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-two-way.png new file mode 100644 index 0000000000..c7761074d0 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-two-way.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-settings.png b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-settings.png new file mode 100644 index 0000000000..32870de750 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-settings.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample.png new file mode 100644 index 0000000000..8aa35f755c Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-create-new-widget-button.png b/ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-create-new-widget-button.png new file mode 100644 index 0000000000..c9bc57dbf7 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-create-new-widget-button.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png b/ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png new file mode 100644 index 0000000000..32deda06cf Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png new file mode 100644 index 0000000000..b69cd945af Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/external-js-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/external-js-widget-sample.png new file mode 100644 index 0000000000..1c82113ac3 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/external-js-widget-sample.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/latest-values-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/latest-values-widget-sample.png new file mode 100644 index 0000000000..70ed92dee6 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/latest-values-widget-sample.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/static-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/static-widget-sample.png new file mode 100644 index 0000000000..f804da9e93 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/static-widget-sample.png differ diff --git a/ui-ngx/src/assets/help/images/widget/editor/examples/timeseries-widget-sample.png b/ui-ngx/src/assets/help/images/widget/editor/examples/timeseries-widget-sample.png new file mode 100644 index 0000000000..f9cb92baf7 Binary files /dev/null and b/ui-ngx/src/assets/help/images/widget/editor/examples/timeseries-widget-sample.png differ diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index bf8f10ab73..00cd06c893 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -737,7 +737,7 @@ "close": "Dialog schließen" }, "edge": { - "edge": "Rand", + "edge": "Edge", "edge-instances": "Kanteninstanzen", "edge-file": "Edge-Datei", "management": "Rand verwalten", diff --git a/ui-ngx/src/assets/locale/locale.constant-el_GR.json b/ui-ngx/src/assets/locale/locale.constant-el_GR.json index 153621e69a..92988533a3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-el_GR.json +++ b/ui-ngx/src/assets/locale/locale.constant-el_GR.json @@ -596,8 +596,8 @@ "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό", "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.", "make-private-dashboard": "Κάνε το dashboard ιδιωτικό", - "socialshare-text": "'{{dashboardTitle}}' powered by EyeTech", - "socialshare-title": "'{{dashboardTitle}}' powered by EyeTech", + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard", "select-dashboard": "Επιλογή dashboard", "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.", "dashboard-required": "Απαιτείται dashboard.", @@ -2496,7 +2496,7 @@ "domain-name": "Όνομα Domain", "help-link-base-url": "Base url για συνδέσμους βοηθείας", "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας", - "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud", + "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: thingsboard.io", "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας", "platform-name": "Όνομα πλατφόρμας", "platform-version": "Έκδοση πλατφόρμας", diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 16788b9adc..3982bfedda 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -158,6 +158,7 @@ "user-lockout-notification-email": "In case user account lockout, send notification to email", "domain-name": "Domain name", "domain-name-unique": "Domain name and protocol need to unique.", + "domain-name-max-length": "Domain name should be less than 256", "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", "oauth2": { "access-token-uri": "Access token URI", @@ -174,21 +175,28 @@ "client-authentication-method": "Client authentication method", "client-id": "Client ID", "client-id-required": "Client ID is required.", + "client-id-max-length": "Client ID should be less than 256", "client-secret": "Client secret", "client-secret-required": "Client secret is required.", + "client-secret-max-length": "Client secret should be less than 2049", "custom-setting": "Custom settings", "customer-name-pattern": "Customer name pattern", + "customer-name-pattern-max-length": "Customer name pattern should be less than 256", "default-dashboard-name": "Default dashboard name", + "default-dashboard-name-max-length": "Default dashboard name should be less than 256", "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", "email-attribute-key": "Email attribute key", "email-attribute-key-required": "Email attribute key is required.", + "email-attribute-key-max-length": "Email attribute key should be less than 32", "first-name-attribute-key": "First name attribute key", + "first-name-attribute-key-max-length": "First name attribute key should be less than 32", "general": "General", "jwk-set-uri": "JSON Web Key URI", "last-name-attribute-key": "Last name attribute key", + "last-name-attribute-key-max-length": "Last name attribute key should be less than 32", "login-button-icon": "Login button icon", "login-button-label": "Provider label", "login-button-label-placeholder": "Login with $(Provider label)", @@ -197,6 +205,7 @@ "mapper": "Mapper", "new-domain": "New domain", "oauth2": "OAuth2", + "password-max-length": "Password should be less than 256", "redirect-uri-template": "Redirect URI template", "copy-redirect-uri": "Copy redirect URI", "registration-id": "Registration ID", @@ -206,14 +215,17 @@ "scope-required": "Scope is required.", "tenant-name-pattern": "Tenant name pattern", "tenant-name-pattern-required": "Tenant name pattern is required.", + "tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256", "tenant-name-strategy": "Tenant name strategy", "type": "Mapper type", "uri-pattern-error": "Invalid URI format.", "url": "URL", "url-pattern": "Invalid URL format.", "url-required": "URL is required.", + "url-max-length": "URL should be less than 256", "user-info-uri": "User info URI", "user-info-uri-required": "User info URI is required.", + "username-max-length": "User name should be less than 256", "user-name-attribute-name": "User name attribute key", "user-name-attribute-name-required": "User name attribute key is required", "protocol": "Protocol", @@ -370,6 +382,7 @@ "management": "Asset management", "view-assets": "View Assets", "add": "Add Asset", + "asset-type-max-length": "Asset type should be less than 256", "assign-to-customer": "Assign to customer", "assign-asset-to-customer": "Assign Asset(s) To Customer", "assign-asset-to-customer-text": "Please select the assets to assign to the customer", @@ -392,6 +405,8 @@ "asset-types": "Asset types", "name": "Name", "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "label-max-length": "Label should be less than 256", "description": "Description", "type": "Type", "type-required": "Type is required.", @@ -451,6 +466,7 @@ "scope-shared": "Shared attributes", "add": "Add attribute", "key": "Key", + "key-max-length": "Key should be less than 256", "last-update-time": "Last update time", "key-required": "Attribute key is required.", "value": "Value", @@ -592,7 +608,10 @@ "address2": "Address 2", "phone": "Phone", "email": "Email", - "no-address": "No address" + "no-address": "No address", + "state-max-length": "State length should be less than 256", + "phone-max-length": "Phone number should be less than 256", + "city-max-length": "Specified city should be less than 256" }, "common": { "username": "Username", @@ -648,6 +667,7 @@ "manage-dashboards": "Manage dashboards", "title": "Title", "title-required": "Title is required.", + "title-max-length": "Title should be less than 256", "description": "Description", "details": "Details", "events": "Events", @@ -705,6 +725,7 @@ "select-widget-subtitle": "List of available widget types", "delete": "Delete dashboard", "title-required": "Title is required.", + "title-max-length": "Title should be less than 256", "description": "Description", "details": "Details", "dashboard-details": "Dashboard details", @@ -891,6 +912,7 @@ "management": "Device management", "view-devices": "View Devices", "device-alias": "Device alias", + "device-type-max-length": "Device type should be less than 256", "aliases": "Device aliases", "no-alias-matching": "'{{alias}}' not found.", "no-aliases-found": "No aliases found.", @@ -1003,6 +1025,8 @@ "device-types": "Device types", "name": "Name", "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "label-max-length": "Label should be less than 256", "description": "Description", "label": "Label", "events": "Events", @@ -1055,6 +1079,7 @@ "set-default": "Make device profile default", "delete": "Delete device profile", "copyId": "Copy device profile Id", + "name-max-length": "Name should be less than 256", "new-device-profile-name": "Device profile name", "new-device-profile-name-required": "Device profile name is required.", "name": "Name", @@ -1096,6 +1121,10 @@ "mqtt-device-payload-type": "MQTT device payload", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Enable compatibility with other payload formats.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "When enabled, the platform will use a Protobuf payload format by default. If parsing fails, the platform will attempt to use JSON payload format. Useful for backward compatibility during firmware updates. For example, the initial release of the firmware uses Json, while the new release uses Protobuf. During the process of firmware update for the fleet of devices, it is required to support both Protobuf and JSON simultaneously. The compatibility mode introduces slight performance degradation, so it is recommended to disable this mode once all devices are updated.", + "mqtt-use-json-format-for-default-downlink-topics": "Use Json format for default downlink topics", + "mqtt-use-json-format-for-default-downlink-topics-hint": "When enabled, the platform will use Json payload format to push attributes and RPC via the following topics: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. This setting does not impact attribute and rpc subscriptions sent using new (v2) topics: v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Where $request_id is an integer request identifier.", "snmp-add-mapping": "Add SNMP mapping", "snmp-mapping-not-configured": "No mapping for OID to timeseries/telemetry configured", "snmp-timseries-or-attribute-name": "Timeseries/attribute name for mapping", @@ -1140,6 +1169,7 @@ "alarm-type": "Alarm type", "alarm-type-required": "Alarm type is required.", "alarm-type-unique": "Alarm type must be unique within the device profile alarm rules.", + "alarm-type-max-length": "Alarm type should be less than 256", "create-alarm-pattern": "Create {{alarmType}} alarm", "create-alarm-rules": "Create alarm rules", "no-create-alarm-rules": "No create conditions configured", @@ -1412,6 +1442,9 @@ "edge": "Edge", "edge-instances": "Edge instances", "edge-file": "Edge file", + "name-max-length": "Name should be less than 256", + "label-max-length": "Label should be less than 256", + "type-max-length": "Type should be less than 256", "management": "Edge management", "no-edges-matching": "No edges matching '{{entity}}' were found.", "add": "Add Edge", @@ -1428,9 +1461,11 @@ "name-required": "Name is required.", "edge-license-key": "Edge License Key", "edge-license-key-required": "Edge License Key is required.", + "edge-license-key-max-length": "Edge License Key should be less than 31", "edge-license-key-hint": "To obtain your license please navigate to the pricing page and select the best license option for your case.", "cloud-endpoint": "Cloud Endpoint", "cloud-endpoint-required": "Cloud Endpoint is required.", + "cloud-endpoint-max-length": "Cloud Endpoint should be less than 256", "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard CE/PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", "description": "Description", "details": "Details", @@ -1746,6 +1781,8 @@ "created-time": "Created time", "name": "Name", "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", + "type-max-length": "Entity view type should be less than 256", "description": "Description", "events": "Events", "details": "Details", @@ -2370,6 +2407,7 @@ "selected-package": "{ count, plural, 1 {1 package} other {# packages} } selected", "title": "Title", "title-required": "Title is required.", + "title-max-length": "Title should be less than 256", "types": { "firmware": "Firmware", "software": "Software" @@ -2380,6 +2418,7 @@ "version-required": "Version is required.", "version-tag": "Version Tag", "version-tag-hint": "Custom tag should match the package version reported by your device.", + "version-max-length": "Version should be less than 256", "warning-after-save-no-edit": "Once the package is uploaded, you will not be able to modify title, version, device profile and package type." }, "position": { @@ -2422,6 +2461,7 @@ "delete": "Delete relation", "relation-type": "Relation type", "relation-type-required": "Relation type is required.", + "relation-type-max-length": "Relation type should be less than 256", "any-relation-type": "Any type", "add": "Add relation", "edit": "Edit relation", @@ -2466,7 +2506,8 @@ "selected-resources": "{ count, plural, 1 {1 resource} other {# resources} } selected", "system": "System", "title": "Title", - "title-required": "Title is required." + "title-required": "Title is required.", + "title-max-length": "Title should be less than 256" }, "rulechain": { "rulechain": "Rule chain", @@ -2475,6 +2516,7 @@ "delete": "Delete rule chain", "name": "Name", "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", "description": "Description", "add": "Add Rule Chain", "set-root": "Make rule chain root", @@ -2537,6 +2579,7 @@ "add": "Add rule node", "name": "Name", "name-required": "Name is required.", + "name-max-length": "Name should be less than 256", "type": "Type", "description": "Description", "delete": "Delete rule node", @@ -2544,6 +2587,7 @@ "deselect-all-objects": "Deselect all nodes and connections", "delete-selected-objects": "Delete selected nodes and connections", "delete-selected": "Delete selected", + "create-nested-rulechain": "Create nested rule chain", "select-all": "Select all", "copy-selected": "Copy selected", "deselect-all": "Deselect all", @@ -2574,6 +2618,8 @@ "type-external-details": "Interacts with external system", "type-rule-chain": "Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", + "type-flow": "Flow", + "type-flow-details": "Organizes message flow", "type-input": "Input", "type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node", "type-unknown": "Unknown", @@ -2617,6 +2663,7 @@ "add-tenant-text": "Add new tenant", "no-tenants-text": "No tenants found", "tenant-details": "Tenant details", + "title-max-length": "Title should be less than 256", "delete-tenant-title": "Are you sure you want to delete the tenant '{{tenantTitle}}'?", "delete-tenant-text": "Be careful, after the confirmation the tenant and all related data will become unrecoverable.", "delete-tenants-title": "Are you sure you want to delete { count, plural, 1 {1 tenant} other {# tenants} }?", @@ -2646,6 +2693,7 @@ "edit": "Edit tenant profile", "tenant-profile-details": "Tenant profile details", "no-tenant-profiles-text": "No tenant profiles found", + "name-max-length": "Name should be less than 256", "search": "Search tenant profiles", "selected-tenant-profiles": "{ count, plural, 1 {1 tenant profile} other {# tenant profiles} } selected", "no-tenant-profiles-matching": "No tenant profile matching '{{entity}}' were found.", @@ -2993,6 +3041,7 @@ "delete": "Delete widgets bundle", "title": "Title", "title-required": "Title is required.", + "title-max-length": "Title should be less than 256", "description": "Description", "image-preview": "Image preview", "add-widgets-bundle-text": "Add new widgets bundle", @@ -3026,7 +3075,7 @@ "title": "Title", "title-tooltip": "Title Tooltip", "general-settings": "General settings", - "display-title": "Display title", + "display-title": "Display widget title", "drop-shadow": "Drop shadow", "enable-fullscreen": "Enable fullscreen", "background-color": "Background color", @@ -3035,7 +3084,7 @@ "margin": "Margin", "widget-style": "Widget style", "title-style": "Title style", - "mobile-mode-settings": "Mobile mode settings", + "mobile-mode-settings": "Mobile mode", "order": "Order", "height": "Height", "mobile-hide": "Hide widget in mobile mode", @@ -3044,6 +3093,7 @@ "timewindow": "Timewindow", "use-dashboard-timewindow": "Use dashboard timewindow", "display-timewindow": "Display timewindow", + "legend": "Legend", "display-legend": "Display legend", "datasources": "Datasources", "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", @@ -3071,9 +3121,13 @@ "delete-action": "Delete action", "delete-action-title": "Delete widget action", "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?", + "title-icon": "Title icon", "display-icon": "Display title icon", "icon-color": "Icon color", - "icon-size": "Icon size" + "icon-size": "Icon size", + "advanced-settings": "Advanced settings", + "data-settings": "Data settings", + "no-data-display-message": "\"No data to display\" alternative message" }, "widget-type": { "import": "Import widget type", @@ -3187,7 +3241,40 @@ "update-timeseries": "Update timeseries", "value": "Value" }, - "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type" + "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", + "maps": { + "select-entity": "Select entity", + "select-entity-hint": "Hint: after selection click at the map to set position", + "tooltips": { + "placeMarker": "Click to place marker", + "firstVertex": "Click to place first point", + "continueLine": "Click to continue drawing", + "finishLine": "Click any existing marker to finish", + "finishPoly": "Click first marker to finish and save", + "finishRect": "Click to finish and save", + "startCircle": "Click to place circle center", + "finishCircle": "Click to finish circle", + "placeCircleMarker": "Click to place circle marker" + }, + "actions": { + "finish": "Finish", + "cancel": "Cancel", + "removeLastVertex": "Remove last point" + }, + "buttonTitles": { + "drawMarkerButton": "Create marker", + "drawPolyButton": "Create polygon", + "drawLineButton": "Create Polyline", + "drawCircleButton": "Create Circle", + "drawRectButton": "Create rectangle", + "editButton": "Edit mode", + "dragButton": "Drag-drop mode", + "cutButton": "Cut polygon area", + "deleteButton": "Remove", + "drawCircleMarkerButton": "Create circle marker", + "rotateButton": "Rotate polygon" + } + } }, "icon": { "icon": "Icon", diff --git a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json index facf0f8534..b7f6dfebfb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-fr_FR.json +++ b/ui-ngx/src/assets/locale/locale.constant-fr_FR.json @@ -1470,7 +1470,7 @@ "rulechain-required": "Chaîne de règles requise", "rulechains": "Chaînes de règles", "select-rulechain": "Sélectionner la chaîne de règles", - "set-root": "Rend la chaîne de règles racine (root) ", + "set-root": "Rendre la chaîne de règles racine (root) ", "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", "system": "Système", @@ -1498,7 +1498,7 @@ "edge-template-root": "Racine du modèle", "search": "Rechercher des chaînes de règles", "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné", - "open-rulechain": "Chaîne de règles ouverte", + "open-rulechain": "Ouvrir la Chaîne de règles", "assign-to-edge": "Attribuer à Bordure", "edge-rulechain": "Chaîne de règles Bordure", "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?" diff --git a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json index f18d62cad2..94da40c3c1 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ka_GE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ka_GE.json @@ -492,7 +492,7 @@ "make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის", "make-private-dashboard": "აქციე დეშბორდი პრივატულად", "socialshare-text": "სოციალური ტექსტი", - "socialshare-title": "'{{dashboardTitle}}' Powered by Giot", + "socialshare-title": "'{{dashboardTitle}}' Powered by ThingsBoard", "select-dashboard": "აირჩიე დეშბორდი", "no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.", "dashboard-required": "დეშბორდი აუცილებელია", diff --git a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json index 7daa8e5802..3563aef32e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-lv_LV.json +++ b/ui-ngx/src/assets/locale/locale.constant-lv_LV.json @@ -453,8 +453,8 @@ "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?", "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.", "make-private-dashboard": "Veidot paneli privātu", - "socialshare-text": "'{{dashboardTitle}}' atbalsts no TeT", - "socialshare-title": "'{{dashboardTitle}}' atbalsts no TeT", + "socialshare-text": "'{{dashboardTitle}}' atbalsts no ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' atbalsts no ThingsBoard", "select-dashboard": "Atlasīt paneli", "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.", "dashboard-required": "Penelis ir nepieciešams.", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index 35df70e300..66d99281bb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -1631,7 +1631,7 @@ "advanced": "Дополнительно", "title": "Название", "general-settings": "Общие настройки", - "display-title": "Показать название", + "display-title": "Показать название на виджете", "drop-shadow": "Тень", "enable-fullscreen": "Во весь экран", "background-color": "Цвет фона", @@ -1640,7 +1640,7 @@ "margin": "Margin", "widget-style": "Стиль виджета", "title-style": "Стиль названия", - "mobile-mode-settings": "Настройки мобильного режима", + "mobile-mode-settings": "Мобильный режим", "order": "Порядок", "height": "Высота", "units": "Специальный символ после значения", @@ -1648,6 +1648,7 @@ "timewindow": "Временное окно", "use-dashboard-timewindow": "Использовать временное окно дашборда", "display-timewindow": "Показывать временное окно", + "legend": "Легенда", "display-legend": "Показать легенду", "datasources": "Источники данных", "maximum-datasources": "Максимальной количество источников данных равно {{count}}", @@ -1673,9 +1674,13 @@ "delete-action": "Удалить действие", "delete-action-title": "Удалить действие виджета", "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", - "display-icon": "Показывать иконку в названии", + "title-icon": "Иконка в названии виджета", + "display-icon": "Показывать иконку в названии виджета", "icon-color": "Цвет иконки", - "icon-size": "Размер иконки" + "icon-size": "Размер иконки", + "advanced-settings": "Расширенные настройки", + "data-settings": "Настройки данных", + "no-data-display-message": "\"Нет данных для отображения\" альтернативный текст" }, "widget-type": { "import": "Импортировать тип виджета", diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 257a3519a9..43691918dd 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -2202,7 +2202,7 @@ "advanced": "Додатково", "title": "Назва", "general-settings": "Загальні налаштування", - "display-title": "Відобразити назву", + "display-title": "Відобразити назву у віджеті", "drop-shadow": "Тінь", "enable-fullscreen": "Увімкнути повноекранний режим", "enable-data-export": "Увімкнути експорт даних", @@ -2212,7 +2212,7 @@ "margin": "Границі", "widget-style": "Стиль віджетів", "title-style": "Стиль заголовка", - "mobile-mode-settings": "Налаштування мобільного режиму", + "mobile-mode-settings": "мобільний режим", "order": "Порядок", "height": "Висота", "units": "Спеціальний символ після значення", @@ -2220,6 +2220,7 @@ "timewindow": "Вікно часу", "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", "display-timewindow": "Показувати вікно часу", + "legend": "Легенда", "display-legend": "Показати легенду", "datasources": "Джерела даних", "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", @@ -2245,9 +2246,13 @@ "delete-action": "Видалити дію", "delete-action-title": "Видалити дію віджета", "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?", - "display-icon": "Показувати іконку у назві", + "title-icon": "Іконка у назві віджету", + "display-icon": "Показувати іконку у назві віджету", "icon-color": "Колір іконки", - "icon-size": "Розмір іконки" + "icon-size": "Розмір іконки", + "advanced-settings": "Розширені налаштування", + "data-settings": "Налаштування даних", + "no-data-display-message": "\"Немає данних для відображення\" альтернативний текст" }, "widget-type": { "import": "Імпортувати тип віджета", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index cb135a221a..270fd6ad4e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -1924,6 +1924,68 @@ "sign-in": "登录 ", "username": "用户名(电子邮件)" }, + "ota-update": { + "add": "添加包", + "assign-firmware": "分配的固件", + "assign-firmware-required": "分配的固件必填", + "assign-software": "分配的软件", + "assign-software-required": "分配的软件必填", + "auto-generate-checksum": "自动生成校验和", + "checksum": "校验和", + "checksum-hint": "如果校验和为空,会自动生成", + "checksum-algorithm": "校验和算法", + "checksum-copied-message": "包校验和已复制到剪贴板", + "change-firmware": "固件的更改可能会导致 { count, plural, 1 {1 个设备} other {# 个设备} } 的更新。", + "change-software": "软件的更改可能会导致 { count, plural, 1 {1 个设备} other {# 个设备} } 的更新。", + "chose-compatible-device-profile": "上传的包仅适用于具有所选配置文件的设备。", + "chose-firmware-distributed-device": "选择将分发到设备的固件", + "chose-software-distributed-device": "选择将分发到设备的软件", + "content-type": "内容类型", + "copy-checksum": "复制校验和", + "copy-direct-url": "复制直接URL", + "copyId": "复制包ID", + "copied": "已复制!", + "delete": "删除包", + "delete-ota-update-text": "小心,OTA升级确认后不可恢复。", + "delete-ota-update-title": "您确定要删除OTA升级'{{title}}'吗?", + "delete-ota-updates-text": "小心,确认后所有选中的OTA升级将被删除。", + "delete-ota-updates-title": "您确定要删除 { count, plural, 1 {1 OTA升级} other {# OTA升级} } 吗?", + "description": "描述", + "direct-url": "直接URL", + "direct-url-copied-message": "包直接URL已复制到剪贴板", + "direct-url-required": "直接URL必填", + "download": "下载包", + "drop-file": "拖放打包文件或点击选择要上传的文件。", + "file-name": "文件名", + "file-size": "文件大小", + "file-size-bytes": "文件大小(以字节为单位)", + "idCopiedMessage": "包ID已被复制到剪贴板", + "no-firmware-matching": "没有找到与'{{entity}}'匹配的兼容固件OTA升级包。", + "no-firmware-text": "没有提供兼容的固件OTA升级包。", + "no-packages-text": "没有找到包", + "no-software-matching": "没有找到匹配 '{{entity}}' 的兼容软件OTA升级包。", + "no-software-text": "没有提供兼容的软件OTA升级包。", + "ota-update": "OTA 升级", + "ota-update-details": "OTA 升级详情", + "ota-updates": "OTA 升级", + "package-type": "包类型", + "packages-repository": "包仓库", + "search": "搜索包", + "selected-package": "{ count, plural, 1 {1 个包} other {# 个包} } 选中", + "title": "标题", + "title-required": "标题必填。", + "types": { + "firmware": "固件", + "software": "软件" + }, + "upload-binary-file": "上传二进制文件", + "use-external-url": "使用外部URL", + "version": "版本", + "version-required": "版本必填。", + "version-tag": "版本标签", + "version-tag-hint": "自定义标签应与您设备报告的软件包版本相匹配。", + "warning-after-save-no-edit": "上传包后,您将无法修改标题、版本、设备配置文件和包类型。" + }, "position": { "bottom": "底部", "left": "左侧", diff --git a/ui-ngx/src/assets/shadow.png b/ui-ngx/src/assets/shadow.png new file mode 100644 index 0000000000..fc2f271799 Binary files /dev/null and b/ui-ngx/src/assets/shadow.png differ diff --git a/ui-ngx/src/environments/environment.ts b/ui-ngx/src/environments/environment.ts index fd558aa87b..35f971c1d2 100644 --- a/ui-ngx/src/environments/environment.ts +++ b/ui-ngx/src/environments/environment.ts @@ -35,4 +35,4 @@ export const environment = { * This import should be commented out in production mode because it will have a negative impact * on performance if an error is thrown. */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/ui-ngx/src/polyfills.ts b/ui-ngx/src/polyfills.ts index 9da0bd3053..7c910914db 100644 --- a/ui-ngx/src/polyfills.ts +++ b/ui-ngx/src/polyfills.ts @@ -73,7 +73,7 @@ */ import './zone-flags'; -import 'zone.js/dist/zone'; // Included with Angular CLI. +import 'zone.js'; // Included with Angular CLI. import 'core-js/es/array'; /*************************************************************************************************** diff --git a/ui-ngx/src/scss/animations.scss b/ui-ngx/src/scss/animations.scss index 2d8bf3469e..5a0fcff2b5 100644 --- a/ui-ngx/src/scss/animations.scss +++ b/ui-ngx/src/scss/animations.scss @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import "~compass-sass-mixins/lib/animate"; @keyframes tbMoveFromTopFade { from { diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss index 2d2495b998..cb3e24a9d7 100644 --- a/ui-ngx/src/scss/mixins.scss +++ b/ui-ngx/src/scss/mixins.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@use "sass:math"; + @mixin tb-mat-icon-size($size) { width: #{$size}px; min-width: #{$size}px; @@ -51,7 +53,7 @@ $x1: $x0; @for $i from 1 through 10 { - $x1: $x0 - ($x0 * $x0 - abs($r)) / (2 * $x0); + $x1: $x0 - math.div($x0 * $x0 - abs($r), 2 * $x0); $x0: $x1; } diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 1dd50841a3..edf7e0e2bb 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -498,10 +498,12 @@ mat-label { color: #0F161D; line-height: normal; font-weight: 500; - padding: 30px 32px 10px; border-bottom: none; margin: 0; } + & > #{$heading} { + padding: 30px 32px 10px; + } } p { @@ -575,6 +577,9 @@ mat-label { margin-bottom: 30px; overflow: hidden; table-layout: fixed; + &.auto { + table-layout: auto; + } & > thead { background-color: #f9fbff; @@ -587,10 +592,12 @@ mat-label { padding: 12px 16px; text-align: left; margin: 0; - word-break: break-word; @media screen and (max-width: 400px) { font-size: 12px; padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } } } } @@ -608,11 +615,13 @@ mat-label { padding: 12px 16px; text-align: left; margin: 0; - word-break: break-word; color: rgba(15, 22, 29, 0.8); @media screen and (max-width: 400px) { font-size: 12px; padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } } } } @@ -732,6 +741,9 @@ mat-label { -moz-user-select: none; -ms-user-select: none; user-select: none; + &.multiline { + right: 44px; + } p { padding: 8px; top: 1px; diff --git a/ui-ngx/src/test.ts b/ui-ngx/src/test.ts index 2fad0ab73c..567a569ad0 100644 --- a/ui-ngx/src/test.ts +++ b/ui-ngx/src/test.ts @@ -16,7 +16,7 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/dist/zone-testing'; +import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, diff --git a/ui-ngx/src/theme.scss b/ui-ngx/src/theme.scss index 25e1131e63..6821a0706b 100644 --- a/ui-ngx/src/theme.scss +++ b/ui-ngx/src/theme.scss @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@import '~@angular/material/theming'; +@use '~@angular/material' as mat; @import '~@mat-datetimepicker/core/datetimepicker/datetimepicker-theme.scss'; @import './scss/constants'; -@include mat-core(); +@include mat.core(); $tb-primary-color: #305680; $tb-secondary-color: #527dad; @@ -41,39 +41,39 @@ $tb-mat-indigo: ( A400: #3d5afe, A700: #304ffe, contrast: ( - 50: $dark-primary-text, - 100: $dark-primary-text, - 200: $dark-primary-text, - 300: $light-primary-text, - 400: $light-primary-text, - 500: $light-primary-text, - 600: $light-primary-text, - 700: $light-primary-text, - 800: $light-primary-text, - 900: $light-primary-text, - A100: $dark-primary-text, - A200: $light-primary-text, - A400: $light-primary-text, - A700: $light-primary-text, + 50: rgba(black, 0.87), + 100: rgba(black, 0.87), + 200: rgba(black, 0.87), + 300: white, + 400: white, + 500: white, + 600: white, + 700: white, + 800: white, + 900: white, + A100: rgba(black, 0.87), + A200: white, + A400: white, + A700: white, ) ); -$tb-primary: mat-palette($tb-mat-indigo); -$tb-accent: mat-palette($mat-deep-orange); +$tb-primary: mat.define-palette($tb-mat-indigo); +$tb-accent: mat.define-palette(mat.$deep-orange-palette); -$background: (background: map_get($mat-grey, 200)); +$background: (background: map_get(mat.$grey-palette, 200)); -$tb-theme-background: map_merge($mat-light-theme-background, $background); +$tb-theme-background: map_merge(mat.$light-theme-background-palette, $background); -$tb-mat-theme: mat-light-theme( +$tb-mat-theme: mat.define-light-theme( $tb-primary, $tb-accent ); $tb-theme: map_merge($tb-mat-theme, (background: $tb-theme-background)); -$primary: mat-color($tb-primary); -$accent: mat-color($tb-accent); +$primary: mat.get-color-from-palette($tb-primary); +$accent: mat.get-color-from-palette($tb-accent); $tb-dark-mat-indigo: ( 50: #e8eaf6, @@ -91,24 +91,24 @@ $tb-dark-mat-indigo: ( A400: #3d5afe, A700: #304ffe, contrast: ( - 50: $dark-primary-text, - 100: $dark-primary-text, - 200: $dark-primary-text, - 300: $dark-primary-text, - 400: $dark-primary-text, + 50: rgba(black, 0.87), + 100: rgba(black, 0.87), + 200: rgba(black, 0.87), + 300: rgba(black, 0.87), + 400: rgba(black, 0.87), 500: map_get($tb-mat-indigo, 900), - 600: $light-primary-text, - 700: $light-primary-text, - 800: $light-primary-text, - 900: $light-primary-text, - A100: $dark-primary-text, - A200: $dark-primary-text, - A400: $dark-primary-text, - A700: $dark-primary-text, + 600: white, + 700: white, + 800: white, + 900: white, + A100: rgba(black, 0.87), + A200: rgba(black, 0.87), + A400: rgba(black, 0.87), + A700: rgba(black, 0.87), ) ); -$tb-dark-primary: mat-palette($tb-dark-mat-indigo); +$tb-dark-primary: mat.define-palette($tb-dark-mat-indigo); $tb-dark-theme-background: ( status-bar: black, @@ -119,22 +119,22 @@ $tb-dark-theme-background: ( dialog: map_get($tb-dark-mat-indigo, 800), disabled-button: rgba(white, 0.12), raised-button: map-get($tb-dark-mat-indigo, 50), - focused-button: $light-focused, + focused-button: rgba(white, 0.12), selected-button: map_get($tb-dark-mat-indigo, 900), selected-disabled-button: map_get($tb-dark-mat-indigo, 800), disabled-button-toggle: black, unselected-chip: map_get($tb-dark-mat-indigo, 700), disabled-list-option: black, - tooltip: map_get($mat-grey, 700), + tooltip: map_get(mat.$grey-palette, 700), ); -@function get-tb-dark-theme($primary, $accent, $warn: mat-palette($mat-red)) { +@function get-tb-dark-theme($primary, $accent, $warn: mat.define-palette(mat.$red-palette)) { @return ( primary: $primary, accent: $accent, warn: $warn, is-dark: true, - foreground: $mat-dark-theme-foreground, + foreground: mat.$dark-theme-foreground-palette, background: $tb-dark-theme-background, ); } @@ -153,30 +153,33 @@ $tb-dark-theme: get-tb-dark-theme( mat-fab-toolbar { .mat-fab-toolbar-background { - background: mat-color($background, app-bar); - color: mat-color($foreground, text); + background: mat.get-color-from-palette($background, app-bar); + color: mat.get-color-from-palette($foreground, text); } &.mat-primary { .mat-fab-toolbar-background { - @include _mat-toolbar-color($primary); + background: mat.get-color-from-palette($primary); + color: mat.get-color-from-palette($primary, default-contrast); } } &.mat-accent { .mat-fab-toolbar-background { - @include _mat-toolbar-color($accent); + background: mat.get-color-from-palette($accent); + color: mat.get-color-from-palette($accent, default-contrast); } } &.mat-warn { .mat-fab-toolbar-background { - @include _mat-toolbar-color($warn); + background: mat.get-color-from-palette($warn); + color: mat.get-color-from-palette($warn, default-contrast); } } } } @mixin _mat-toolbar-inverse-color($palette) { - background: mat-color($palette, default-contrast); - color: $dark-primary-text; + background: mat.get-color-from-palette($palette, default-contrast); + color: rgba(black, 0.87); } @mixin mat-fab-toolbar-inverse-theme($theme) { @@ -188,8 +191,8 @@ $tb-dark-theme: get-tb-dark-theme( mat-fab-toolbar { .mat-fab-toolbar-background { - background: mat-color($background, app-bar); - color: mat-color($foreground, text); + background: mat.get-color-from-palette($background, app-bar); + color: mat.get-color-from-palette($foreground, text); } &.mat-primary { .mat-fab-toolbar-background { @@ -201,15 +204,15 @@ $tb-dark-theme: get-tb-dark-theme( @include _mat-toolbar-inverse-color($primary); button.mat-icon-button { mat-icon { - color: mat-color($primary); + color: mat.get-color-from-palette($primary); } } } } .mat-fab { &.mat-primary { - background: mat-color($primary, default-contrast); - color: mat-color($primary); + background: mat.get-color-from-palette($primary, default-contrast); + color: mat.get-color-from-palette($primary); } } } @@ -221,7 +224,7 @@ $tb-dark-theme: get-tb-dark-theme( mat-toolbar{ &.mat-hue-3 { - background-color: mat-color($primary, 'A100'); + background-color: mat.get-color-from-palette($primary, 'A100'); } } @@ -233,12 +236,12 @@ $tb-dark-theme: get-tb-dark-theme( } .tb-default { - @include angular-material-theme($tb-theme); + @include mat.all-component-themes($tb-theme); @include mat-datetimepicker-theme($tb-theme); @include tb-components-theme($tb-theme); } .tb-dark { - @include angular-material-theme($tb-dark-theme); + @include mat.all-component-themes($tb-dark-theme); } diff --git a/ui-ngx/src/tsconfig.app.json b/ui-ngx/src/tsconfig.app.json index 5f897102b7..fb8ea9b284 100644 --- a/ui-ngx/src/tsconfig.app.json +++ b/ui-ngx/src/tsconfig.app.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "../out-tsc/app", "types": ["node", "jquery", "flot", "tooltipster", "tinycolor2", "js-beautify", - "react", "react-dom", "jstree", "raphael", "canvas-gauges", "leaflet", "leaflet.markercluster", "leaflet-editable"] + "react", "react-dom", "jstree", "raphael", "canvas-gauges", "systemjs"] }, "angularCompilerOptions": { "fullTemplateTypeCheck": true diff --git a/ui-ngx/src/typings/leaflet-geoman-extend.d.ts b/ui-ngx/src/typings/leaflet-geoman-extend.d.ts new file mode 100644 index 0000000000..1a936fe780 --- /dev/null +++ b/ui-ngx/src/typings/leaflet-geoman-extend.d.ts @@ -0,0 +1,1520 @@ +/// +/// 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. +/// + +/* tslint:disable:adjacent-overload-signatures unified-signatures */ + +import * as L from 'leaflet'; + +declare module 'leaflet-pm' { + + /** + * Extends built in leaflet Layer Options. + */ + interface LayerOptions { + pmIgnore?: boolean; + snapIgnore?: boolean; + } + + /** + * Extends built in leaflet Map Options. + */ + interface MapOptions { + pmIgnore?: boolean; + } + + /** + * Extends built in leaflet Map. + */ + interface Map { + pm: PM.PMMap; + } + + /** + * Extends built in leaflet Path. + */ + interface Path { + pm: PM.PMLayer; + } + /** + * Extends built in leaflet ImageOverlay. + */ + interface ImageOverlay { + pm: PM.PMLayer; + } + + /** + * Extends built in leaflet LayerGroup. + */ + interface LayerGroup { + pm: PM.PMLayerGroup; + } + + /** + * Extends built in leaflet Polyline. + */ + interface Polyline { + /** Returns true if Line or Polygon has a self intersection. */ + hasSelfIntersection(): boolean; + } + + /** + * Extends @types/leaflet events... + * + * Todo: This is kind of a mess, and it makes all these event handlers show + * up on Layers and Map. Leaflet itself is based around Evented, and @types/leaflet + * makes this very hard to work around. + * + */ + interface Evented { + /****************************************** + * + * AVAILABLE ON MAP + LAYER, THESE ARE OK ON EVENTED. + * + ********************************************/ + + /** Fired when a layer is removed via Removal Mode. */ + on(type: 'pm:remove', fn: PM.RemoveEventHandler): this; + once(type: 'pm:remove', fn: PM.RemoveEventHandler): this; + off(type: 'pm:remove', fn?: PM.RemoveEventHandler): this; + + /** Fired when the layer being cut. Draw+Edit Mode */ + on(type: 'pm:cut', fn: PM.CutEventHandler): this; + once(type: 'pm:cut', fn: PM.CutEventHandler): this; + off(type: 'pm:cut', fn?: PM.CutEventHandler): this; + + /** Fired when rotation is enabled for a layer. */ + on(type: 'pm:rotateenable', fn: PM.RotateEnableEventHandler): this; + once(type: 'pm:rotateenable', fn: PM.RotateEnableEventHandler): this; + off(type: 'pm:rotateenable', fn?: PM.RotateEnableEventHandler): this; + + /** Fired when rotation is disabled for a layer. */ + on(type: 'pm:rotatedisable', fn: PM.RotateDisableEventHandler): this; + once(type: 'pm:rotatedisable', fn: PM.RotateDisableEventHandler): this; + off(type: 'pm:rotatedisable', fn?: PM.RotateDisableEventHandler): this; + + /** Fired when rotation starts on a layer. */ + on(type: 'pm:rotatestart', fn: PM.RotateStartEventHandler): this; + once(type: 'pm:rotatestart', fn: PM.RotateStartEventHandler): this; + off(type: 'pm:rotatestart', fn?: PM.RotateStartEventHandler): this; + + /** Fired when a layer is rotated. */ + on(type: 'pm:rotate', fn: PM.RotateEventHandler): this; + once(type: 'pm:rotate', fn: PM.RotateEventHandler): this; + off(type: 'pm:rotate', fn?: PM.RotateEventHandler): this; + + /** Fired when rotation ends on a layer. */ + on(type: 'pm:rotateend', fn: PM.RotateEndEventHandler): this; + once(type: 'pm:rotateend', fn: PM.RotateEndEventHandler): this; + off(type: 'pm:rotateend', fn?: PM.RotateEndEventHandler): this; + + /****************************************** + * + * TODO: DRAW/EDIT MODE EVENTS LAYER ONLY + * + ********************************************/ + + /** Fired during a marker move/drag. */ + on(type: 'pm:snapdrag', fn: PM.SnapEventHandler): this; + once(type: 'pm:snapdrag', fn: PM.SnapEventHandler): this; + off(type: 'pm:snapdrag', fn?: PM.SnapEventHandler): this; + + /** Fired when a vertex is snapped. */ + on(type: 'pm:snap', fn: PM.SnapEventHandler): this; + once(type: 'pm:snap', fn: PM.SnapEventHandler): this; + off(type: 'pm:snap', fn?: PM.SnapEventHandler): this; + + /** Fired when a vertex is unsnapped. */ + on(type: 'pm:unsnap', fn: PM.SnapEventHandler): this; + once(type: 'pm:unsnap', fn: PM.SnapEventHandler): this; + off(type: 'pm:unsnap', fn?: PM.SnapEventHandler): this; + + /** Called when the center of a circle is placed/moved. */ + on(type: 'pm:centerplaced', fn: PM.CenterPlacedEventHandler): this; + once(type: 'pm:centerplaced', fn: PM.CenterPlacedEventHandler): this; + off(type: 'pm:centerplaced', fn?: PM.CenterPlacedEventHandler): this; + + /****************************************** + * + * TODO: CUT/EDIT MODE EVENTS LAYER ONLY + * + ********************************************/ + + /** Fired when a layer is edited. */ + on(type: 'pm:edit', fn: PM.EditEventHandler): this; + once(type: 'pm:edit', fn: PM.EditEventHandler): this; + off(type: 'pm:edit', fn?: PM.EditEventHandler): this; + + /****************************************** + * + * TODO: DRAW MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Drawing Mode is toggled. */ + on( + type: 'pm:globaldrawmodetoggled', + fn: PM.GlobalDrawModeToggledEventHandler, + context?: any + ): L.Evented; + once( + type: 'pm:globaldrawmodetoggled', + fn: PM.GlobalDrawModeToggledEventHandler, + context?: any + ): L.Evented; + off( + type: 'pm:globaldrawmodetoggled', + fn?: PM.GlobalDrawModeToggledEventHandler, + context?: any + ): L.Evented; + + /** Called when drawing mode is enabled. Payload includes the shape type and working layer. */ + on( + type: 'pm:drawstart', + fn: PM.DrawStartEventHandler, + context?: any + ): L.Evented; + once( + type: 'pm:drawstart', + fn: PM.DrawStartEventHandler, + context?: any + ): L.Evented; + off( + type: 'pm:drawstart', + fn?: PM.DrawStartEventHandler, + context?: any + ): L.Evented; + + /** Called when drawing mode is disabled. Payload includes the shape type. */ + on( + type: 'pm:drawend', + fn: PM.DrawEndEventHandler, + context?: any + ): L.Evented; + once( + type: 'pm:drawend', + fn: PM.DrawEndEventHandler, + context?: any + ): L.Evented; + off( + type: 'pm:drawend', + fn?: PM.DrawEndEventHandler, + context?: any + ): L.Evented; + + /** Called when drawing mode is disabled. Payload includes the shape type. */ + on(type: 'pm:create', fn: PM.CreateEventHandler, context?: any): L.Evented; + once( + type: 'pm:create', + fn: PM.CreateEventHandler, + context?: any + ): L.Evented; + off( + type: 'pm:create', + fn?: PM.CreateEventHandler, + context?: any + ): L.Evented; + + /****************************************** + * + * TODO: DRAW MODE EVENTS ON LAYER ONLY + * + ********************************************/ + + /** Called when a new vertex is added. */ + on(type: 'pm:vertexadded', fn: PM.VertexAddedEventHandler): this; + once(type: 'pm:vertexadded', fn: PM.VertexAddedEventHandler): this; + off(type: 'pm:vertexadded', fn?: PM.VertexAddedEventHandler): this; + + /****************************************** + * + * TODO: EDIT MODE EVENTS ON LAYER ONLY + * + ********************************************/ + + /** Fired when edit mode is disabled and a layer is edited and its coordinates have changed. */ + on(type: 'pm:update', fn: PM.UpdateEventHandler): this; + once(type: 'pm:update', fn: PM.UpdateEventHandler): this; + off(type: 'pm:update', fn?: PM.UpdateEventHandler): this; + + /** Fired when edit mode on a layer is enabled. */ + on(type: 'pm:enable', fn: PM.EnableEventHandler): this; + once(type: 'pm:enable', fn: PM.EnableEventHandler): this; + off(type: 'pm:enable', fn?: PM.EnableEventHandler): this; + + /** Fired when edit mode on a layer is disabled. */ + on(type: 'pm:disable', fn: PM.DisableEventHandler): this; + once(type: 'pm:disable', fn: PM.DisableEventHandler): this; + off(type: 'pm:disable', fn?: PM.DisableEventHandler): this; + + /** Fired when a vertex is added. */ + on(type: 'pm:vertexadded', fn: PM.VertexAddedEventHandler2): this; + once(type: 'pm:vertexadded', fn: PM.VertexAddedEventHandler2): this; + off(type: 'pm:vertexadded', fn?: PM.VertexAddedEventHandler2): this; + + /** Fired when a vertex is removed. */ + on(type: 'pm:vertexremoved', fn: PM.VertexRemovedEventHandler): this; + once(type: 'pm:vertexremoved', fn: PM.VertexRemovedEventHandler): this; + off(type: 'pm:vertexremoved', fn?: PM.VertexRemovedEventHandler): this; + + /** Fired when a vertex is clicked. */ + on(type: 'pm:vertexclick', fn: PM.VertexClickEventHandler): this; + once(type: 'pm:vertexclick', fn: PM.VertexClickEventHandler): this; + off(type: 'pm:vertexclick', fn?: PM.VertexClickEventHandler): this; + + /** Fired when dragging of a marker which corresponds to a vertex starts. */ + on(type: 'pm:markerdragstart', fn: PM.MarkerDragStartEventHandler): this; + once(type: 'pm:markerdragstart', fn: PM.MarkerDragStartEventHandler): this; + off(type: 'pm:markerdragstart', fn?: PM.MarkerDragStartEventHandler): this; + + /** Fired when dragging a vertex-marker. */ + on(type: 'pm:markerdrag', fn: PM.MarkerDragEventHandler): this; + once(type: 'pm:markerdrag', fn: PM.MarkerDragEventHandler): this; + off(type: 'pm:markerdrag', fn?: PM.MarkerDragEventHandler): this; + + /** Fired when dragging of a vertex-marker ends. */ + on(type: 'pm:markerdragend', fn: PM.MarkerDragEndEventHandler): this; + once(type: 'pm:markerdragend', fn: PM.MarkerDragEndEventHandler): this; + off(type: 'pm:markerdragend', fn?: PM.MarkerDragEndEventHandler): this; + + /** Fired when coords of a layer are reset. E.g. by self-intersection.. */ + on(type: 'pm:layerreset', fn: PM.LayerResetEventHandler): this; + once(type: 'pm:layerreset', fn: PM.LayerResetEventHandler): this; + off(type: 'pm:layerreset', fn?: PM.LayerResetEventHandler): this; + + /** When allowSelfIntersection: false, this event is fired as soon as a self-intersection is detected. */ + on(type: 'pm:intersect', fn: PM.IntersectEventHandler): this; + once(type: 'pm:intersect', fn: PM.IntersectEventHandler): this; + off(type: 'pm:intersect', fn?: PM.IntersectEventHandler): this; + + /****************************************** + * + * TODO: EDIT MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Edit Mode is toggled. */ + on( + type: 'pm:globaleditmodetoggled', + fn: PM.GlobalEditModeToggledEventHandler + ): this; + once( + type: 'pm:globaleditmodetoggled', + fn: PM.GlobalEditModeToggledEventHandler + ): this; + off( + type: 'pm:globaleditmodetoggled', + fn?: PM.GlobalEditModeToggledEventHandler + ): this; + + /****************************************** + * + * TODO: DRAG MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Drag Mode is toggled. */ + on( + type: 'pm:globaldragmodetoggled', + fn: PM.GlobalDragModeToggledEventHandler + ): this; + once( + type: 'pm:globaldragmodetoggled', + fn: PM.GlobalDragModeToggledEventHandler + ): this; + off( + type: 'pm:globaldragmodetoggled', + fn?: PM.GlobalDragModeToggledEventHandler + ): this; + + /****************************************** + * + * TODO: DRAG MODE EVENTS ON LAYER ONLY + * + ********************************************/ + + /** Fired when a layer starts being dragged. */ + on(type: 'pm:dragstart', fn: PM.DragStartEventHandler): this; + once(type: 'pm:dragstart', fn: PM.DragStartEventHandler): this; + off(type: 'pm:dragstart', fn?: PM.DragStartEventHandler): this; + + /** Fired when a layer is dragged. */ + on(type: 'pm:drag', fn: PM.DragEventHandler): this; + once(type: 'pm:drag', fn: PM.DragEventHandler): this; + off(type: 'pm:drag', fn?: PM.DragEventHandler): this; + + /** Fired when a layer stops being dragged. */ + on(type: 'pm:dragend', fn: PM.DragEndEventHandler): this; + once(type: 'pm:dragend', fn: PM.DragEndEventHandler): this; + off(type: 'pm:dragend', fn?: PM.DragEndEventHandler): this; + + /****************************************** + * + * TODO: REMOVE MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Removal Mode is toggled. */ + on( + type: 'pm:globalremovalmodetoggled', + fn: PM.GlobalRemovalModeToggledEventHandler + ): this; + once( + type: 'pm:globalremovalmodetoggled', + fn: PM.GlobalRemovalModeToggledEventHandler + ): this; + off( + type: 'pm:globalremovalmodetoggled', + fn?: PM.GlobalRemovalModeToggledEventHandler + ): this; + + /****************************************** + * + * TODO: CUT MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when a layer is removed via Removal Mode. */ + on( + type: 'pm:globalcutmodetoggled', + fn: PM.GlobalCutModeToggledEventHandler + ): this; + once( + type: 'pm:globalcutmodetoggled', + fn: PM.GlobalCutModeToggledEventHandler + ): this; + off( + type: 'pm:globalcutmodetoggled', + fn?: PM.GlobalCutModeToggledEventHandler + ): this; + + /****************************************** + * + * TODO: ROTATE MODE EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when Rotate Mode is toggled. */ + on( + type: 'pm:globalrotatemodetoggled', + fn: PM.GlobalRotateModeToggledEventHandler + ): this; + once( + type: 'pm:globalrotatemodetoggled', + fn: PM.GlobalRotateModeToggledEventHandler + ): this; + off( + type: 'pm:globalrotatemodetoggled', + fn?: PM.GlobalRotateModeToggledEventHandler + ): this; + + /****************************************** + * + * TODO: TRANSLATION EVENTS ON MAP ONLY + * + ********************************************/ + + /** Standard Leaflet event. Fired when any layer is removed. */ + on(type: 'pm:langchange', fn: PM.LangChangeEventHandler): this; + once(type: 'pm:langchange', fn: PM.LangChangeEventHandler): this; + off(type: 'pm:langchange', fn?: PM.LangChangeEventHandler): this; + + /****************************************** + * + * TODO: CONTROL EVENTS ON MAP ONLY + * + ********************************************/ + + /** Fired when a Toolbar button is clicked. */ + on(type: 'pm:buttonclick', fn: PM.ButtonClickEventHandler): this; + once(type: 'pm:buttonclick', fn: PM.ButtonClickEventHandler): this; + off(type: 'pm:buttonclick', fn?: PM.ButtonClickEventHandler): this; + + /** Fired when a Toolbar action is clicked. */ + on(type: 'pm:actionclick', fn: PM.ActionClickEventHandler): this; + once(type: 'pm:actionclick', fn: PM.ActionClickEventHandler): this; + off(type: 'pm:actionclick', fn?: PM.ActionClickEventHandler): this; + + /****************************************** + * + * TODO: Keyboard EVENT ON MAP ONLY + * + ********************************************/ + + /** Fired when `keydown` or `keyup` on the document is fired. */ + on(type: 'pm:keyevent', fn: PM.KeyboardKeyEventHandler): this; + once(type: 'pm:keyevent', fn: PM.KeyboardKeyEventHandler): this; + off(type: 'pm:keyevent', fn?: PM.KeyboardKeyEventHandler): this; + } + + namespace PM { + /** Supported shape names. 'ImageOverlay' is in Edit Mode only. Also accepts custom shape name. */ + type SUPPORTED_SHAPES = + | 'Marker' + | 'Circle' + | 'Line' + | 'Rectangle' + | 'Polygon' + | 'Cut' + | 'CircleMarker' + | 'ImageOverlay' + | string; + + /** + * Changes default registration of leaflet-geoman on leaflet layers. + * + * @param optIn - if true, a layers pmIgnore property has to be set to false to get initiated. + */ + function setOptIn(optIn: boolean): void; + + /** + * Enable leaflet-geoman on an ignored layer. + * + * @param layer - re-reads layer.options.pmIgnore to initialize leaflet-geoman. + */ + function reInitLayer(layer: L.Layer): void; + + /** + * PM map interface. + */ + interface PMMap + extends PMDrawMap, + PMEditMap, + PMDragMap, + PMRemoveMap, + PMCutMap, + PMRotateMap { + Toolbar: PMMapToolbar; + + Keyboard: PMMapKeyboard; + + /** Adds the Toolbar to the map. */ + addControls(options?: ToolbarOptions): void; + + /** Toggle the visiblity of the Toolbar. */ + removeControls(): void; + + /** Returns true if the Toolbar is visible on the map. */ + controlsVisible(): boolean; + + /** Toggle the visiblity of the Toolbar. */ + toggleControls(): void; + + setLang( + lang: + | 'cz' + | 'da' + | 'de' + | 'el' + | 'en' + | 'es' + | 'fa' + | 'fr' + | 'hu' + | 'id' + | 'it' + | 'nl' + | 'no' + | 'pl' + | 'pt_br' + | 'ro' + | 'ru' + | 'sv' + | 'tr' + | 'ua' + | 'zh' + | 'zh_tw', + customTranslations?: Translations, + fallbackLanguage?: string + ): void; + + /** Set globalOptions and apply them. */ + setGlobalOptions(options: GlobalOptions): void; + + /** Apply the current globalOptions to all existing layers. */ + applyGlobalOptions(): void; + + /** Returns the globalOptions. */ + getGlobalOptions(): GlobalOptions; + } + + class Translations { + tooltips?: { + placeMarker?: string; + firstVertex?: string; + continueLine?: string; + finishLine?: string; + finishPoly?: string; + finishRect?: string; + startCircle?: string; + finishCircle?: string; + placeCircleMarker?: string; + }; + + actions?: { + finish?: string; + cancel?: string; + removeLastVertex?: string; + }; + + buttonTitles?: { + drawMarkerButton?: string; + drawPolyButton?: string; + drawLineButton?: string; + drawCircleButton?: string; + drawRectButton?: string; + editButton?: string; + dragButton?: string; + cutButton?: string; + deleteButton?: string; + drawCircleMarkerButton?: string; + }; + } + + type ACTION_NAMES = 'cancel' | 'removeLastVertex' | 'finish' | 'finishMode'; + + class Action { + text: string; + onClick?: (e: any) => void; + } + + type TOOLBAR_CONTROL_ORDER = + | 'drawMarker' + | 'drawCircleMarker' + | 'drawPolyline' + | 'drawRectangle' + | 'drawPolygon' + | 'drawCircle' + | 'editMode' + | 'dragMode' + | 'cutPolygon' + | 'removalMode' + | 'rotateMode' + | string; + + interface PMMapToolbar { + /** Pass an array of button names to reorder the buttons in the Toolbar. */ + changeControlOrder(order?: TOOLBAR_CONTROL_ORDER[]): void; + + /** Receive the current order with. */ + getControlOrder(): TOOLBAR_CONTROL_ORDER[]; + + /** The position of a block (draw, edit, custom, options⭐) in the Toolbar can be changed. If not set, the value */ + /** from position of the Toolbar is taken. */ + setBlockPosition( + block: 'draw' | 'edit' | 'custom' | 'options', + position: L.ControlPosition + ): void; + + /** Returns a Object with the positions for all blocks */ + getBlockPositions(): BlockPositions; + + /** To add a custom Control to the Toolbar */ + createCustomControl(options: CustomControlOptions): void; + + /** Creates a copy of a draw Control. Returns the drawInstance and the control. */ + copyDrawControl( + copyInstance: string, + options?: CustomControlOptions + ): void; + + /** Change the actions of an existing button. */ + changeActionsOfControl( + name: string, + actions: (ACTION_NAMES | Action)[] + ): void; + + /** Disable button by control name */ + setButtonDisabled(name: TOOLBAR_CONTROL_ORDER, state: boolean): void; + } + + type KEYBOARD_EVENT_TYPE = 'current' | 'keydown' | 'keyup'; + + interface PMMapKeyboard { + /** Pass an array of button names to reorder the buttons in the Toolbar. */ + getLastKeyEvent(type: KEYBOARD_EVENT_TYPE[]): KeyboardKeyEventHandler; + + /** Returns the current pressed key. [KeyboardEvent.key](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key). */ + getPressedKey(): string; + + /** Returns true if the `Shift` key is currently pressed. */ + isShiftKeyPressed(): boolean; + + /** Returns true if the `Alt` key is currently pressed. */ + isAltKeyPressed(): boolean; + + /** Returns true if the `Ctrl` key is currently pressed. */ + isCtrlKeyPressed(): boolean; + + /** Returns true if the `Meta` key is currently pressed. */ + isMetaKeyPressed(): boolean; + } + + interface Button { + /** Actions */ + actions: (ACTION_NAMES | Action)[]; + + /** Function fired after clicking the control. */ + afterClick: () => void; + + /** CSS class with the Icon. */ + className: string; + + /** If true, other buttons will be disabled on click (default: true) */ + disableOtherButtons: boolean; + + /** Control can be toggled. */ + doToggle: boolean; + + /** Extending Class f. ex. Line, Polygon, ... L.PM.Draw.EXTENDINGCLASS */ + jsClass: string; + + /** Function fired when clicking the control. */ + onClick: () => void; + + position: L.ControlPosition; + + /** Text showing when you hover the control. */ + title: string; + + /** Toggle state true -> enabled, false -> disabled (default: false) */ + toggleStatus: boolean; + + /** Block of the control. 'options' is ⭐ only. */ + tool?: 'draw' | 'edit' | 'custom' | 'options'; + } + + interface CustomControlOptions { + /** Name of the control */ + name: string; + + /** Block of the control. 'options' is ⭐ only. */ + block?: 'draw' | 'edit' | 'custom' | 'options'; + + /** Text showing when you hover the control. */ + title?: string; + + /** CSS class with the Icon. */ + className?: string; + + /** Function fired when clicking the control. */ + onClick?: () => void; + + /** Function fired after clicking the control. */ + afterClick?: () => void; + + /** Actions */ + actions?: (ACTION_NAMES | Action)[]; + + /** Control can be toggled. */ + toggle?: boolean; + + /** Control is disabled. */ + disabled?: boolean; + } + + type PANE = + | 'mapPane' + | 'tilePane' + | 'overlayPane' + | 'shadowPane' + | 'markerPane' + | 'tooltipPane' + | 'popupPane' + | string; + + interface GlobalOptions extends DrawModeOptions, EditModeOptions { + /** Add the created layers to a layergroup instead to the map. */ + layerGroup?: L.Map | L.LayerGroup; + + /** Prioritize the order of snapping. Default: ['Marker','CircleMarker','Circle','Line','Polygon','Rectangle']. */ + snappingOrder: SUPPORTED_SHAPES[]; + + /** Defines in which panes the layers and helper vertices are created. Default: */ + /** { vertexPane: 'markerPane', layerPane: 'overlayPane', markerPane: 'markerPane' } */ + panes: { vertexPane: PANE; layerPane: PANE; markerPane: PANE }; + } + + interface PMDrawMap { + + /** Draw */ + Draw: Draw; + /** Enable Draw Mode with the passed shape. */ + enableDraw(shape: SUPPORTED_SHAPES, options?: DrawModeOptions): void; + + /** Disable all drawing */ + disableDraw(shape?: SUPPORTED_SHAPES): void; + + /** Returns true if global Draw Mode is enabled. false when disabled. */ + globalDrawModeEnabled(): boolean; + + /** Customize the style of the drawn layer. Only for L.Path layers. */ + /** Shapes can be excluded with a ignoreShapes array or merged with the current style with merge: true in optionsModifier. */ + setPathOptions( + options: L.PathOptions, + optionsModifier?: { ignoreShapes?: SUPPORTED_SHAPES[]; merge?: boolean } + ): void; + + /** Returns all Geoman layers on the map as array. Pass true to get a L.FeatureGroup. */ + getGeomanLayers(asFeatureGroup?: boolean): L.FeatureGroup | L.Layer[]; + + /** Returns all Geoman draw layers on the map as array. Pass true to get a L.FeatureGroup. */ + getGeomanDrawLayers(asFeatureGroup?: boolean): L.FeatureGroup | L.Layer[]; + } + + interface PMEditMap { + /** Enables edit mode. The passed options are preserved, even when the mode is enabled via the Toolbar */ + enableGlobalEditMode(options?: EditModeOptions): void; + + /** Disables global edit mode. */ + disableGlobalEditMode(): void; + + /** Toggles global edit mode. */ + toggleGlobalEditMode(options?: EditModeOptions): void; + + /** Returns true if global edit mode is enabled. false when disabled. */ + globalEditModeEnabled(): boolean; + } + + interface PMDragMap { + /** Enables global drag mode. */ + enableGlobalDragMode(): void; + + /** Disables global drag mode. */ + disableGlobalDragMode(): void; + + /** Toggles global drag mode. */ + toggleGlobalDragMode(): void; + + /** Returns true if global drag mode is enabled. false when disabled. */ + globalDragModeEnabled(): boolean; + } + + interface PMRemoveMap { + /** Enables global removal mode. */ + enableGlobalRemovalMode(): void; + + /** Disables global removal mode. */ + disableGlobalRemovalMode(): void; + + /** Toggles global removal mode. */ + toggleGlobalRemovalMode(): void; + + /** Returns true if global removal mode is enabled. false when disabled. */ + globalRemovalModeEnabled(): boolean; + } + + interface PMCutMap { + /** Enables global cut mode. */ + enableGlobalCutMode(options?: CutModeOptions): void; + + /** Disables global cut mode. */ + disableGlobalCutMode(): void; + + /** Toggles global cut mode. */ + toggleGlobalCutMode(options?: CutModeOptions): void; + + /** Returns true if global cut mode is enabled. false when disabled. */ + globalCutModeEnabled(): boolean; + } + + interface PMRotateMap { + /** Enables global rotate mode. */ + enableGlobalRotateMode(): void; + + /** Disables global rotate mode. */ + disableGlobalRotateMode(): void; + + /** Toggles global rotate mode. */ + toggleGlobalRotateMode(): void; + + /** Returns true if global rotate mode is enabled. false when disabled. */ + globalRotateModeEnabled(): boolean; + } + + interface PMRotateLayer { + /** Enables rotate mode on the layer. */ + enableRotate(): void; + + /** Disables rotate mode on the layer. */ + disableRotate(): void; + + /** Toggles rotate mode on the layer. */ + rotateEnabled(): void; + + /** Rotates the layer by x degrees. */ + rotateLayer(degrees: number): void; + + /** Rotates the layer to x degrees. */ + rotateLayerToAngle(degrees: number): void; + + /** Returns the angle of the layer in degrees. */ + getAngle(): number; + } + + interface Draw { + /** Array of available shapes. */ + getShapes(): SUPPORTED_SHAPES[]; + + /** Returns the active shape. */ + getActiveShape(): SUPPORTED_SHAPES; + + /** Set path options */ + setPathOptions(options: L.PathOptions): void; + + /** Set options */ + setOptions(options: DrawModeOptions): void; + + /** Get options */ + getOptions(): DrawModeOptions; + } + + interface CutModeOptions { + allowSelfIntersection?: boolean; + } + + type VertexValidationHandler = (e: { + layer: L.Layer; + marker: L.Marker; + event: any; + }) => boolean; + + interface EditModeOptions { + /** Enable snapping to other layers vertices for precision drawing. Can be disabled by holding the ALT key (default:true). */ + snappable?: boolean; + + /** The distance to another vertex when a snap should happen (default:20). */ + snapDistance?: number; + + /** Allow self intersections (default:true). */ + allowSelfIntersection?: boolean; + + /** Allow self intersections (default:true). */ + allowSelfIntersectionEdit?: boolean; + + /** Disable the removal of markers via right click / vertices via removeVertexOn. (default:false). */ + preventMarkerRemoval?: boolean; + + /** If true, vertex removal that cause a layer to fall below their minimum required vertices will remove the entire layer. */ + /** If false, these vertices can't be removed. Minimum vertices are 2 for Lines and 3 for Polygons (default:true). */ + removeLayerBelowMinVertexCount?: boolean; + + /** Defines which layers should dragged with this layer together. */ + /** true syncs all layers in the same LayerGroup(s) or you pass an `Array` of layers to sync. (default:false). */ + syncLayersOnDrag?: L.Layer[] | boolean; + + /** Edit-Mode for the layer can disabled (`pm.enable()`). (default:true). */ + allowEditing?: boolean; + + /** Removing can be disabled for the layer. (default:true). */ + allowRemoval?: boolean; + + /** Layer can be prevented from cutting. (default:true). */ + allowCutting?: boolean; + + /** Layer can be prevented from rotation. (default:true). */ + allowRotation?: boolean; + + /** Dragging can be disabled for the layer. (default:true). */ + draggable?: boolean; + + /** Leaflet layer event to add a vertex to a Line or Polygon, like dblclick. (default:click). */ + addVertexOn?: + | 'click' + | 'dblclick' + | 'mousedown' + | 'mouseover' + | 'mouseout' + | 'contextmenu'; + + /** A function for validation if a vertex (of a Line / Polygon) is allowed to add. */ + /** It passes a object with `[layer, marker, event}`. For example to check if the layer */ + /** has a certain property or if the `Ctrl` key is pressed. (default:undefined). */ + addVertexValidation?: undefined | VertexValidationHandler; + + /** Leaflet layer event to remove a vertex from a Line or Polygon, like dblclick. (default:contextmenu). */ + removeVertexOn?: + | 'click' + | 'dblclick' + | 'mousedown' + | 'mouseover' + | 'mouseout' + | 'contextmenu'; + + /** A function for validation if a vertex (of a Line / Polygon) is allowed to remove. */ + /** It passes a object with `[layer, marker, event}`. For example to check if the layer has a certain property */ + /** or if the `Ctrl` key is pressed. */ + removeVertexValidation?: undefined | VertexValidationHandler; + + /** A function for validation if a vertex / helper-marker is allowed to move / drag. It passes a object with */ + /** `[layer, marker, event}`. For example to check if the layer has a certain property or if the `Ctrl` key is pressed. */ + moveVertexValidation?: undefined | VertexValidationHandler; + + /** Shows only n markers closest to the cursor. Use -1 for no limit (default:-1). */ + limitMarkersToCount?: number; + + /** Shows markers when under the given zoom level ⭐ */ + limitMarkersToZoom?: number; + + /** Shows only markers in the viewport ⭐ */ + limitMarkersToViewport?: boolean; + + /** Shows markers only after the layer was clicked ⭐ */ + limitMarkersToClick?: boolean; + + /** Pin shared vertices/markers together during edit ⭐ */ + pinning?: boolean; + + /** Hide the middle Markers in edit mode from Polyline and Polygon. */ + hideMiddleMarkers?: boolean; + } + + interface DrawModeOptions { + /** Enable snapping to other layers vertices for precision drawing. Can be disabled by holding the ALT key (default:true). */ + snappable?: boolean; + + /** The distance to another vertex when a snap should happen (default:20). */ + snapDistance?: number; + + /** Allow snapping in the middle of two vertices (middleMarker)(default:false). */ + snapMiddle?: boolean; + + /** Allow snapping between two vertices. (default: true) */ + snapSegment?: boolean; + + /** Require the last point of a shape to be snapped. (default: false). */ + requireSnapToFinish?: boolean; + + /** Show helpful tooltips for your user (default:true). */ + tooltips?: boolean; + + /** Allow self intersections (default:true). */ + allowSelfIntersection?: boolean; + + /** Leaflet path options for the lines between drawn vertices/markers. (default:{color:'red'}). */ + templineStyle?: L.PathOptions; + + /** Leaflet path options for the helper line between last drawn vertex and the cursor. (default:{color:'red',dashArray:[5,5]}). */ + hintlineStyle?: L.PathOptions; + + /** Leaflet path options for the drawn layer (Only for L.Path layers). (default:null). */ + pathOptions?: L.PathOptions; + + /** Leaflet marker options (only for drawing markers). (default:{draggable:true}). */ + markerStyle?: L.MarkerOptions; + + /** Show a marker at the cursor (default:true). */ + cursorMarker?: boolean; + + /** Leaflet layer event to finish the drawn shape (default:null). */ + finishOn?: + | null + | 'click' + | 'dblclick' + | 'mousedown' + | 'mouseover' + | 'mouseout' + | 'contextmenu' + | 'snap'; + + /** Hide the middle Markers in edit mode from Polyline and Polygon. (default:false). */ + hideMiddleMarkers?: boolean; + + /** Set the min radius of a Circle. (default:null). */ + minRadiusCircle?: number; + + /** Set the max radius of a Circle. (default:null). */ + maxRadiusCircle?: number; + + /** Set the min radius of a CircleMarker when editable is active. (default:null). */ + minRadiusCircleMarker?: number; + + /** Set the max radius of a CircleMarker when editable is active. (default:null). */ + maxRadiusCircleMarker?: number; + + /** Makes a CircleMarker editable like a Circle (default:false). */ + editable?: boolean; + + /** Markers and CircleMarkers are editable during the draw-session */ + /** (you can drag them around immediately after drawing them) (default:true). */ + markerEditable?: boolean; + + /** Draw-Mode stays enabled after finishing a layer to immediately draw the next layer. */ + /** Defaults to true for Markers and CircleMarkers and false for all other layers. */ + continueDrawing?: boolean; + + /** Angel of rectangle. */ + rectangleAngle?: number; + + /** Cut-Mode: Only the passed layers can be cut. Cutted layers are removed from the */ + /** Array until no layers are left anymore and cutting is working on all layers again. (Default: []) */ + layersToCut?: L.Layer[]; + } + + /** + * PM toolbar options. + */ + interface ToolbarOptions { + /** Toolbar position. */ + position?: L.ControlPosition; + + /** The position of each block can be customized. If not set, the value from position is taken. */ + positions?: BlockPositions; + + /** Adds button to draw Markers (default:true) */ + drawMarker?: boolean; + + /** Adds button to draw CircleMarkers (default:true) */ + drawCircleMarker?: boolean; + + /** Adds button to draw Line (default:true) */ + drawPolyline?: boolean; + + /** Adds button to draw Rectangle (default:true) */ + drawRectangle?: boolean; + + /** Adds button to draw Polygon (default:true) */ + drawPolygon?: boolean; + + /** Adds button to draw Circle (default:true) */ + drawCircle?: boolean; + + /** Adds button to toggle edit mode for all layers (default:true) */ + editMode?: boolean; + + /** Adds button to toggle drag mode for all layers (default:true) */ + dragMode?: boolean; + + /** Adds button to cut a hole in a polygon or line (default:true) */ + cutPolygon?: boolean; + + /** Adds a button to remove layers (default:true) */ + removalMode?: boolean; + + /** Adds a button to rotate layers (default:true) */ + rotateMode?: boolean; + + /** All buttons will be displayed as one block Customize Controls (default:false) */ + oneBlock?: boolean; + + /** Shows all draw buttons / buttons in the draw block (default:true) */ + drawControls?: boolean; + + /** Shows all edit buttons / buttons in the edit block (default:true) */ + editControls?: boolean; + + /** Shows all buttons in the custom block (default:true) */ + customControls?: boolean; + + /** Shows all options buttons / buttons in the option block ⭐ */ + optionsControls?: boolean; + + /** Adds a button to toggle the Pinning Option ⭐ */ + pinningOption?: boolean; + + /** Adds a button to toggle the Snapping Option ⭐ */ + snappingOption?: boolean; + } + + /** the position of each block. */ + interface BlockPositions { + /** Draw control position (default:''). '' also refers to this position. */ + draw?: L.ControlPosition; + + /** Edit control position (default:''). */ + edit?: L.ControlPosition; + + /** Custom control position (default:''). */ + custom?: L.ControlPosition; + + /** Options control position (default:'') ⭐ */ + options?: L.ControlPosition; + } + + interface PMEditLayer { + /** Enables edit mode. The passed options are preserved, even when the mode is enabled via the Toolbar */ + enable(options?: EditModeOptions): void; + + /** Sets layer options */ + setOptions(options?: EditModeOptions): void; + + /** Gets layer options */ + getOptions(): EditModeOptions; + + /** Disables edit mode. */ + disable(): void; + + /** Toggles edit mode. Passed options are preserved. */ + toggleEdit(options?: EditModeOptions): void; + + /** Returns true if edit mode is enabled. false when disabled. */ + enabled(): boolean; + + /** Returns true if Line or Polygon has a self intersection. */ + hasSelfIntersection(): boolean; + + /** Removes the layer with the same checks as GlobalRemovalMode. */ + remove(): void; + } + + interface PMDragLayer { + /** Enables dragging for the layer. */ + enableLayerDrag(): void; + + /** Disables dragging for the layer. */ + disableLayerDrag(): void; + + /** Returns if the layer is currently dragging. */ + dragging(): boolean; + + /** Returns if drag mode is enabled for the layer. */ + layerDragEnabled(): boolean; + } + + interface PMLayer extends PMRotateLayer, PMEditLayer, PMDragLayer { + /** Get shape of the layer. */ + getShape(): SUPPORTED_SHAPES; + } + + interface PMLayerGroup { + /** Enables edit mode for all child layers. The passed options are preserved, even when the mode is enabled via the Toolbar */ + enable(options?: EditModeOptions): void; + + /** Disable edit mode for all child layers. */ + disable(): void; + + /** Returns if minimum one layer is enabled. */ + enabled(): boolean; + + /** Toggle enable / disable on all layers. */ + toggleEdit(options?: EditModeOptions): void; + + /** Returns the layers of the LayerGroup. `deep=true` return also the children of LayerGroup children. */ + /** `filterGeoman=true` filter out layers that don't have Leaflet-Geoman or temporary stuff. */ + /** `filterGroupsOut=true` does not return the LayerGroup layers self. */ + /** (Default: `deep=false`,`filterGeoman=true`, `filterGroupsOut=true` ) */ + getLayers( + deep?: boolean, + filterGeoman?: boolean, + filterGroupsOut?: boolean + ): L.Layer[]; + + /** Apply Leaflet-Geoman options to all children. The passed options are preserved, even when the mode is enabled via the Toolbar */ + setOptions(options?: EditModeOptions): void; + + /** Returns the options of the LayerGroup. */ + getOptions(): EditModeOptions; + + /** Returns if currently a layer in the LayerGroup is dragging. */ + dragging(): boolean; + } + + namespace Utils { + /** Returns the translation of the passed path. path = json-string f.ex. tooltips.placeMarker */ + function getTranslation(path: string): string; + + /** Returns the middle LatLng between two LatLngs */ + function calcMiddleLatLng( + map: L.Map, + latlng1: L.LatLng, + latlng2: L.LatLng + ): L.LatLng; + + /** Returns all layers that are available for Geoman */ + function findLayers(map: L.Map): L.Layer[]; + + /** Converts a circle into a polygon with default 60 sides. For CRS.Simple maps `withBearing` needs to be false */ + function circleToPolygon( + circle: L.Circle, + sides?: number, + withBearing?: boolean + ): L.Polygon; + + /** Converts a px-radius (CircleMarker) to meter-radius (Circle). */ + /** The center LatLng is needed because the earth has different projections on different places. */ + function pxRadiusToMeterRadius( + radiusInPx: number, + map: L.Map, + center: L.LatLng + ): number; + } + + /** + * DRAW MODE MAP EVENT HANDLERS + */ + + export type GlobalDrawModeToggledEventHandler = (event: { + enabled: boolean; + shape: PM.SUPPORTED_SHAPES; + map: L.Map; + }) => void; + export type DrawStartEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + workingLayer: L.Layer; + }) => void; + export type DrawEndEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type CreateEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; + + /** + * DRAW MODE LAYER EVENT HANDLERS + */ + + export type VertexAddedEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + workingLayer: L.Layer; + marker: L.Marker; + latlng: L.LatLng; + }) => void; + export type SnapEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + distance: number; + layer: L.Layer; + workingLayer: L.Layer; + marker: L.Marker; + layerInteractedWith: L.Layer; + segement: any; + snapLatLng: L.LatLng; + }) => void; + export type CenterPlacedEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + workingLayer: L.Layer; + latlng: L.LatLng; + }) => void; + + /** + * EDIT MODE LAYER EVENT HANDLERS + */ + + export type EditEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; + export type UpdateEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; + export type EnableEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; + export type DisableEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + }) => void; + export type VertexAddedEventHandler2 = (e: { + layer: L.Layer; + indexPath: number; + latlng: L.LatLng; + marker: L.Marker; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type VertexRemovedEventHandler = (e: { + layer: L.Layer; + indexPath: number; + marker: L.Marker; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type VertexClickEventHandler = (e: { + layer: L.Layer; + indexPath: number; + markerEvent: any; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type MarkerDragStartEventHandler = (e: { + layer: L.Layer; + indexPath: number; + markerEvent: any; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type MarkerDragEventHandler = (e: { + layer: L.Layer; + indexPath: number; + markerEvent: any; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type MarkerDragEndEventHandler = (e: { + layer: L.Layer; + indexPath: number; + markerEvent: any; + shape: PM.SUPPORTED_SHAPES; + intersectionRest: boolean; + }) => void; + export type LayerResetEventHandler = (e: { + layer: L.Layer; + indexPath: number; + markerEvent: any; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type IntersectEventHandler = (e: { + shape: PM.SUPPORTED_SHAPES; + layer: L.Layer; + intersection: L.LatLng; + }) => void; + + /** + * EDIT MODE MAP EVENT HANDLERS + */ + export type GlobalEditModeToggledEventHandler = (event: { + enabled: boolean; + map: L.Map; + }) => void; + + /** + * DRAG MODE MAP EVENT HANDLERS + */ + export type GlobalDragModeToggledEventHandler = (event: { + enabled: boolean; + map: L.Map; + }) => void; + + /** + * DRAG MODE LAYER EVENT HANDLERS + */ + export type DragStartEventHandler = (e: { + layer: L.Layer; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type DragEventHandler = (e: { + layer: L.Layer; + containerPoint: any; + latlng: L.LatLng; + layerPoint: L.Point; + originalEvent: any; + shape: PM.SUPPORTED_SHAPES; + }) => void; + export type DragEndEventHandler = (e: { + layer: L.Layer; + shape: PM.SUPPORTED_SHAPES; + }) => void; + + /** + * REMOVE MODE LAYER EVENT HANDLERS + */ + + export type RemoveEventHandler = (e: { + layer: L.Layer; + shape: PM.SUPPORTED_SHAPES; + }) => void; + + /** + * REMOVE MODE MAP EVENT HANDLERS + */ + export type GlobalRemovalModeToggledEventHandler = (e: { + enabled: boolean; + map: L.Map; + }) => void; + + /** + * CUT MODE MAP EVENT HANDLERS + */ + export type GlobalCutModeToggledEventHandler = (e: { + enabled: boolean; + map: L.Map; + }) => void; + export type CutEventHandler = (e: { + layer: L.Layer; + originalLayer: L.Layer; + shape: PM.SUPPORTED_SHAPES; + }) => void; + + /** + * ROTATE MODE LAYER EVENT HANDLERS + */ + export type RotateEnableEventHandler = (e: { + layer: L.Layer; + helpLayer: L.Layer; + }) => void; + export type RotateDisableEventHandler = (e: { layer: L.Layer }) => void; + export type RotateStartEventHandler = (e: { + layer: L.Layer; + helpLayer: L.Layer; + startAngle: number; + originLatLngs: L.LatLng[]; + }) => void; + export type RotateEventHandler = (e: { + layer: L.Layer; + helpLayer: L.Layer; + startAngle: number; + angle: number; + angleDiff: number; + oldLatLngs: L.LatLng[]; + newLatLngs: L.LatLng[]; + }) => void; + export type RotateEndEventHandler = (e: { + layer: L.Layer; + helpLayer: L.Layer; + startAngle: number; + angle: number; + originLatLngs: L.LatLng[]; + newLatLngs: L.LatLng[]; + }) => void; + + /** + * ROTATE MODE MAP EVENT HANDLERS + */ + export type GlobalRotateModeToggledEventHandler = (e: { + enabled: boolean; + map: L.Map; + }) => void; + + /** + * TRANSLATION EVENT HANDLERS + */ + export type LangChangeEventHandler = (e: { + activeLang: string; + oldLang: string; + fallback: string; + translations: PM.Translations; + }) => void; + + /** + * CONTROL MAP EVENT HANDLERS + */ + export type ButtonClickEventHandler = (e: { + btnName: string; + button: PM.Button; + }) => void; + export type ActionClickEventHandler = (e: { + text: string; + action: string; + btnName: string; + button: PM.Button; + }) => void; + + /** + * KEYBOARD EVENT HANDLERS + */ + export type KeyboardKeyEventHandler = (e: { + focusOn: 'document' | 'map'; + eventType: 'keydown' | 'keyup'; + event: any; + }) => void; + } + + namespace PM { + interface PMMapToolbar { + toggleButton( + name: string, + status: boolean, + disableOthers?: boolean + ): void; + } + } +} diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index 38cdc74820..9963cadb75 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -3,15 +3,18 @@ "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, "sourceMap": true, "declaration": false, - "module": "es2020", + "downlevelIteration": true, + "experimentalDecorators": true, "moduleResolution": "node", + "importHelpers": true, + "target": "es2017", + "module": "es2020", "emitDecoratorMetadata": true, - "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "importHelpers": true, - "target": "es5", "jsx": "react", "typeRoots": [ "node_modules/@types", @@ -20,7 +23,7 @@ "src/typings/jquery.flot.typings.d.ts", "src/typings/jquery.jstree.typings.d.ts", "src/typings/split.js.typings.d.ts", - "src/typings/add-marker.d.ts" + "src/typings/leaflet-geoman-extend.d.ts" ], "paths": { "@app/*": ["src/app/*"], @@ -51,9 +54,14 @@ ] }, "lib": [ - "es2018", - "es2019", + "es2020", "dom" ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": false } } diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index f07fd482e6..f1e5a9abc9 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -2,293 +2,293 @@ # yarn lockfile v1 -"@angular-builders/custom-webpack@~11.1.1": - version "11.1.1" - resolved "https://registry.yarnpkg.com/@angular-builders/custom-webpack/-/custom-webpack-11.1.1.tgz#5ef535f5c51cb2f5a27f53fcfab54176cd1d14d4" - integrity sha512-9l2yC9+QMwWj9IEsOpQtMIBcH87UMZdoR2+8DbIt53ypst/8aVMhMsKmR5n33wRkLlIJ6/ubyXldQymIHzoF1g== - dependencies: - "@angular-devkit/architect" ">=0.1100.0 < 0.1200.0" - "@angular-devkit/build-angular" ">=0.1100.0 < 0.1200.0" - "@angular-devkit/core" "^11.0.0" +"@ampproject/remapping@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-1.0.1.tgz#1398e73e567c2a7992df6554c15bb94a89b68ba2" + integrity sha512-Ta9bMA3EtUHDaZJXqUoT5cn/EecwOp+SXpKJqxDbDuMbLvEMu6YTyDDuvTWeStODfdmXyfMo7LymQyPkN3BicA== + dependencies: + "@jridgewell/resolve-uri" "1.0.0" + sourcemap-codec "1.4.8" + +"@angular-builders/custom-webpack@~12.1.3": + version "12.1.3" + resolved "https://registry.yarnpkg.com/@angular-builders/custom-webpack/-/custom-webpack-12.1.3.tgz#3eda78f573dc6d8b5278bb2d0c20937a75dec0cc" + integrity sha512-CzOkwYnO2Xs+z4kMeJkUALeRjVE69SlrqbEsv2Tao5PsBmFCyT5EEVoSvwOuaxZmajuGaXtz7yBIeK2hYp25/A== + dependencies: + "@angular-devkit/architect" ">=0.1200.0 < 0.1300.0" + "@angular-devkit/build-angular" "^12.0.0" + "@angular-devkit/core" "^12.0.0" lodash "^4.17.15" - ts-node "^9.0.0" + ts-node "^10.0.0" tsconfig-paths "^3.9.0" webpack-merge "^5.7.3" -"@angular-devkit/architect@0.1102.13", "@angular-devkit/architect@>=0.1100.0 < 0.1200.0": - version "0.1102.13" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.13.tgz#0f6b9d38cb8cda928b24404278f227fe8f169fc1" - integrity sha512-v44YF54DQnxwggZKUVCB2WtgfqMW8VG5AtE+nO0RgEtgnOC3e4bB7dZ/n+2Am0LCauYx45ufQDlIespeX09XgA== - dependencies: - "@angular-devkit/core" "11.2.13" - rxjs "6.6.3" - -"@angular-devkit/build-angular@>=0.1100.0 < 0.1200.0", "@angular-devkit/build-angular@^0.1102.13": - version "0.1102.13" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.13.tgz#3d7bac8d471c6f2b7067236151cc5018b2ad1a51" - integrity sha512-1ezVautOtfl16pOT3p7Z9IPd7vSyy6lZ+HZWqMK1AmUjNhsEA2WeuC/XznGatC3K+9IcfV+eSm6cP0iuLyETzw== - dependencies: - "@angular-devkit/architect" "0.1102.13" - "@angular-devkit/build-optimizer" "0.1102.13" - "@angular-devkit/build-webpack" "0.1102.13" - "@angular-devkit/core" "11.2.13" - "@babel/core" "7.12.10" - "@babel/generator" "7.12.11" - "@babel/plugin-transform-async-to-generator" "7.12.1" - "@babel/plugin-transform-runtime" "7.12.10" - "@babel/preset-env" "7.12.11" - "@babel/runtime" "7.12.5" - "@babel/template" "7.12.7" - "@discoveryjs/json-ext" "0.5.2" +"@angular-devkit/architect@0.1202.13", "@angular-devkit/architect@>=0.1200.0 < 0.1300.0": + version "0.1202.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1202.13.tgz#b9b883d62f628a6b31ce071da91e268f61da00ef" + integrity sha512-LXgiidXwBgyWPqqWK4xR1/kCPQTMTzG5w+s7+LvENUZwbcdl6CKrOMjfgjo6WPr6yeq+WWQvPCD4pZ6nXRTm7A== + dependencies: + "@angular-devkit/core" "12.2.13" + rxjs "6.6.7" + +"@angular-devkit/build-angular@^12.0.0", "@angular-devkit/build-angular@^12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-12.2.13.tgz#f5564d3ec9db132956473bb904bb3590482f5b36" + integrity sha512-iJ1P/RZ1hk2n/HtEqB5ohXvHa+Hf0BXShYskSGrn6/klcTb0eJTCREsFxHk7mNEmRIGPWkjbLAslqpPgwiagXg== + dependencies: + "@ampproject/remapping" "1.0.1" + "@angular-devkit/architect" "0.1202.13" + "@angular-devkit/build-optimizer" "0.1202.13" + "@angular-devkit/build-webpack" "0.1202.13" + "@angular-devkit/core" "12.2.13" + "@babel/core" "7.14.8" + "@babel/generator" "7.14.8" + "@babel/helper-annotate-as-pure" "7.14.5" + "@babel/plugin-proposal-async-generator-functions" "7.14.7" + "@babel/plugin-transform-async-to-generator" "7.14.5" + "@babel/plugin-transform-runtime" "7.14.5" + "@babel/preset-env" "7.14.8" + "@babel/runtime" "7.14.8" + "@babel/template" "7.14.5" + "@discoveryjs/json-ext" "0.5.3" "@jsdevtools/coverage-istanbul-loader" "3.0.5" - "@ngtools/webpack" "11.2.13" + "@ngtools/webpack" "12.2.13" ansi-colors "4.1.1" - autoprefixer "10.2.4" babel-loader "8.2.2" browserslist "^4.9.1" - cacache "15.0.5" + cacache "15.2.0" caniuse-lite "^1.0.30001032" circular-dependency-plugin "5.2.2" - copy-webpack-plugin "6.3.2" - core-js "3.8.3" - critters "0.0.7" - css-loader "5.0.1" - cssnano "5.0.2" - file-loader "6.2.0" + copy-webpack-plugin "9.0.1" + core-js "3.16.0" + critters "0.0.12" + css-loader "6.2.0" + css-minimizer-webpack-plugin "3.0.2" + esbuild-wasm "0.13.8" find-cache-dir "3.3.1" - glob "7.1.6" + glob "7.1.7" https-proxy-agent "5.0.0" - inquirer "7.3.3" - jest-worker "26.6.2" + inquirer "8.1.2" karma-source-map-support "1.4.0" less "4.1.1" - less-loader "7.3.0" - license-webpack-plugin "2.3.11" + less-loader "10.0.1" + license-webpack-plugin "2.3.20" loader-utils "2.0.0" - mini-css-extract-plugin "1.3.5" + mini-css-extract-plugin "2.4.2" minimatch "3.0.4" - open "7.4.0" - ora "5.3.0" + open "8.2.1" + ora "5.4.1" parse5-html-rewriting-stream "6.0.1" - pnp-webpack-plugin "1.6.4" - postcss "8.2.14" - postcss-import "14.0.0" - postcss-loader "4.2.0" - raw-loader "4.0.2" - regenerator-runtime "0.13.7" + piscina "3.1.0" + postcss "8.3.6" + postcss-import "14.0.2" + postcss-loader "6.1.1" + postcss-preset-env "6.7.0" + regenerator-runtime "0.13.9" resolve-url-loader "4.0.0" - rimraf "3.0.2" - rollup "2.38.4" - rxjs "6.6.3" - sass "1.32.6" - sass-loader "10.1.1" - semver "7.3.4" - source-map "0.7.3" - source-map-loader "1.1.3" + rxjs "6.6.7" + sass "1.36.0" + sass-loader "12.1.0" + semver "7.3.5" + source-map-loader "3.0.0" source-map-support "0.5.19" - speed-measure-webpack-plugin "1.4.2" - style-loader "2.0.0" + style-loader "3.2.1" stylus "0.54.8" - stylus-loader "4.3.3" - terser "5.5.1" - terser-webpack-plugin "4.2.3" + stylus-loader "6.1.0" + terser "5.7.1" + terser-webpack-plugin "5.1.4" text-table "0.2.0" tree-kill "1.2.2" - webpack "4.44.2" - webpack-dev-middleware "3.7.2" + tslib "2.3.0" + webpack "5.50.0" + webpack-dev-middleware "5.0.0" webpack-dev-server "3.11.2" - webpack-merge "5.7.3" - webpack-sources "2.2.0" + webpack-merge "5.8.0" webpack-subresource-integrity "1.5.2" - worker-plugin "5.0.0" + optionalDependencies: + esbuild "0.13.8" -"@angular-devkit/build-optimizer@0.1102.13": - version "0.1102.13" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.13.tgz#5c60fa88db41f22f296f1ee628f4c6abe90dac95" - integrity sha512-OvTsUVe5cC4oKym1pohVzkn50OLnjK/SuOnMgeWceMDkqmzJEdJqhG1SfnXv+MJiuUDlhyUVbbnFwkj5Y0fXYA== +"@angular-devkit/build-optimizer@0.1202.13": + version "0.1202.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1202.13.tgz#7f0b13368cc2a08b1f4789ab1296a1609f5049ea" + integrity sha512-XX6rX5+mAl+MiIJDvi5N5mBLWOoskhMJ5r/G1PEqv0CMMJSSw60zUTndjxfq/nrX0PtsV3j/aqHN4Sj0w/gumg== dependencies: - loader-utils "2.0.0" source-map "0.7.3" - tslib "2.1.0" - typescript "4.1.5" - webpack-sources "2.2.0" + tslib "2.3.0" + typescript "4.3.5" -"@angular-devkit/build-webpack@0.1102.13": - version "0.1102.13" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.13.tgz#d7d552500713278cc3ed28c3cc669f31b4f285a6" - integrity sha512-5KMq7jUtk5GOuErR6psG8ypyfHL/sITfpgyRXcsqJEqLEbb5MPmaR9gBjz6TSQGUH2/jNiJjdd82h8ckWxfN8g== +"@angular-devkit/build-webpack@0.1202.13": + version "0.1202.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1202.13.tgz#875f9f9677bb10056b52230965dc2f00bd3089dd" + integrity sha512-KafzGyHuU2gBKaSICfMTFP2niTYZ/46DKU94TQ0lCILdJZsj0NE5M/288LSCbYgu2c7srJKz+Rvb+JcYGxIZ1g== dependencies: - "@angular-devkit/architect" "0.1102.13" - "@angular-devkit/core" "11.2.13" - rxjs "6.6.3" + "@angular-devkit/architect" "0.1202.13" + rxjs "6.6.7" -"@angular-devkit/core@11.2.13", "@angular-devkit/core@^11.0.0": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.13.tgz#7829862132555a52009d0af9671b06ffdf9200f1" - integrity sha512-GL97DZXeQCecqZmaIeMfG/XtHv6e7FM+uygMePNF9yn4cml32bSp4P5oRjDMgj7iFl6GIU81n8TstTMmFbt85w== +"@angular-devkit/core@12.2.13", "@angular-devkit/core@^12.0.0": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-12.2.13.tgz#db3929d1bfce71010b37fb7c4e6c33ef80a4f35f" + integrity sha512-9csMF0p+lTvlWnutxxTZ/+pDRMIbXk/TV4MGLbcqUPPfeG3dCRwErns73xLuMTwp9qO/KCLkFqNaM6cGOoqsDA== dependencies: - ajv "6.12.6" + ajv "8.6.2" + ajv-formats "2.1.0" fast-json-stable-stringify "2.1.0" magic-string "0.25.7" - rxjs "6.6.3" + rxjs "6.6.7" source-map "0.7.3" -"@angular-devkit/schematics@11.2.13": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.13.tgz#ce3b55e7f38ca9c40cb9c9c8d0096bd8e1490bb1" - integrity sha512-HMQWs9tsvmsS/QoL+ayxCYqjpP2Qc1x30EJrDU8zPDsaRCe06Uj7VYedBKlHpDGxh0D2F2oRSG1YkXGHr3H/TQ== +"@angular-devkit/schematics@12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-12.2.13.tgz#6464d86fa3ccd0efb5ead46c793cef9ec45c7758" + integrity sha512-LQTv72R5Ma1uowMEeii2wIoDWI4bYQyZvunqPy9jRveBTjli2yVwwcOziGCVyttwlYs46bSdxThgeEvVIako2w== dependencies: - "@angular-devkit/core" "11.2.13" - ora "5.3.0" - rxjs "6.6.3" + "@angular-devkit/core" "12.2.13" + ora "5.4.1" + rxjs "6.6.7" -"@angular/animations@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.14.tgz#cf119ea779bf11bd3759f1355c85e4e8e9e7bb03" - integrity sha512-Heq/nNrCmb3jbkusu+BQszOecfFI/31Oxxj+CDQkqqYpBcswk6bOJLoEE472o+vmgxaXbgeflU9qbIiCQhpMFA== +"@angular/animations@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-12.2.14.tgz#5b74f17f5d58993d92511749fa5ceaf2de6c1495" + integrity sha512-1BR5u32auVePvXNNP96DB2008V+Ku0OGqeZQl2h4XA9xzES/Zk5WllIJZXqRmWMRBVARfXsfb0RdMty9gcaVjA== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/cdk@^11.2.13": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.13.tgz#d54c9187e3b8cf3f8ba190b1edddc08ed2b740de" - integrity sha512-FkE4iCwoLbQxLDUOjV1I7M/6hmpyb7erAjEdWgch7nGRNxF1hqX5Bqf1lvLFKPNCbx5NRI5K7YVAdIUQUR8vug== +"@angular/cdk@^12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-12.2.13.tgz#1fdbe814adfd6b4ff906c6d9c4c6df07b83f09d8" + integrity sha512-zSKRhECyFqhingIeyRInIyTvYErt4gWo+x5DQr0b7YLUbU8DZSwWnG4w76Ke2s4U8T7ry1jpJBHoX/e8YBpGMg== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" optionalDependencies: parse5 "^5.0.0" -"@angular/cli@^11.2.13": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.13.tgz#10534aa2d16e438cf4bb0e912ab28a1eb67dafe1" - integrity sha512-ma5+Iu24XacSY5WrcZ5WJQMv7/RPhWxtbkpr11hndlgUmjmM6dYNGHJOFRXcU7bP7kbOVdd7cZ++2vMGlh0kcg== +"@angular/cli@^12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-12.2.13.tgz#ca586c14a6f83bb4390875be0a0fa709b9a2ae29" + integrity sha512-Yz6MuwdxxP6U2i8iRuCSNZeJxlLDPshT/joymCsFdjwSMGEH9Kk9DdvAfFYfzdHGdHbGrDdASJ4G+uALyNSLxw== dependencies: - "@angular-devkit/architect" "0.1102.13" - "@angular-devkit/core" "11.2.13" - "@angular-devkit/schematics" "11.2.13" - "@schematics/angular" "11.2.13" - "@schematics/update" "0.1102.13" + "@angular-devkit/architect" "0.1202.13" + "@angular-devkit/core" "12.2.13" + "@angular-devkit/schematics" "12.2.13" + "@schematics/angular" "12.2.13" "@yarnpkg/lockfile" "1.1.0" ansi-colors "4.1.1" - debug "4.3.1" + debug "4.3.2" ini "2.0.0" - inquirer "7.3.3" + inquirer "8.1.2" jsonc-parser "3.0.0" - npm-package-arg "8.1.0" - npm-pick-manifest "6.1.0" - open "7.4.0" - ora "5.3.0" - pacote "11.2.4" - resolve "1.19.0" - rimraf "3.0.2" - semver "7.3.4" - symbol-observable "3.0.0" - universal-analytics "0.4.23" + npm-package-arg "8.1.5" + npm-pick-manifest "6.1.1" + open "8.2.1" + ora "5.4.1" + pacote "11.3.5" + resolve "1.20.0" + semver "7.3.5" + symbol-observable "4.0.0" uuid "8.3.2" -"@angular/common@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.14.tgz#52887277b0ae0438e584f9ae97b417ee51a694b5" - integrity sha512-ZSLV/3j7eCTyLf/8g4yBFLWySjiLz3vLJAGWscYoUpnJWMnug1VRu6zoF/COxCbtORgE+Wz6K0uhfS6MziBGVw== +"@angular/common@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-12.2.14.tgz#0cd0d75be8052e8be76966e494f245dc595afe10" + integrity sha512-ffYUYdwZETmFJw0AcWY30WsaWBhJxj/zSmFXWjgEGEGZH56zwbbNwfMZOYZ1jz4haAVxGu+TdXsOl2yMGzN7jQ== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/compiler-cli@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.14.tgz#fdd22aeec25aa2477595bed631d19e977254ecc5" - integrity sha512-A7ltnCp03/EVqK/Q3tVUDsokgz5GHW3dSPGl0Csk7Ys5uBB9ibHTmVt4eiXA4jt0+6Bk+mKxwe5BEDqLvwYFAg== +"@angular/compiler-cli@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-12.2.14.tgz#fdaca73c6941b1231196d980ac8ef02c07ccf36d" + integrity sha512-EktEOF2xnuMsUyanXjZw3hyn7w97NX9h8LJ3O9l27secbjYXhyrao5bmrMILdDTEJNeZSC/OuCga1pvdaJTYmg== dependencies: "@babel/core" "^7.8.6" "@babel/types" "^7.8.6" canonical-path "1.0.0" chokidar "^3.0.0" convert-source-map "^1.5.1" - dependency-graph "^0.7.2" - fs-extra "4.0.2" + dependency-graph "^0.11.0" magic-string "^0.25.0" minimist "^1.2.0" reflect-metadata "^0.1.2" - semver "^6.3.0" + semver "^7.0.0" source-map "^0.6.1" sourcemap-codec "^1.4.8" - tslib "^2.0.0" - yargs "^16.2.0" + tslib "^2.2.0" + yargs "^17.0.0" "@angular/compiler@9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-9.0.0.tgz#87e0bef4c369b6cadae07e3a4295778fc93799d5" integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== -"@angular/compiler@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.14.tgz#9d3ea26b4d2d858aab935f69af9e201f53453a0f" - integrity sha512-XBOK3HgA+/y6Cz7kOX4zcJYmgJ264XnfcbXUMU2cD7Ac+mbNhLPKohWrEiSWalfcjnpf5gRfufQrQP7lpAGu0A== +"@angular/compiler@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-12.2.14.tgz#360940f5e505a00892b46dd28ca69b9a37bdf6af" + integrity sha512-dwmZi+n66IUzRFlGWu9mjXq170ZEsaDvlNLZzaPgs6vZTa4Kt7PWvIF/Y7TMvnVv/uqNG6kOhfmOkf6rfz1Gjg== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" "@angular/core@9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@angular/core/-/core-9.0.0.tgz#227dc53e1ac81824f998c6e76000b7efc522641e" integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== -"@angular/core@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.14.tgz#3ebe298c79d5413dc670d56b7f503bd4d788d4a8" - integrity sha512-vpR4XqBGitk1Faph37CSpemwIYTmJ3pdIVNoHKP6jLonpWu+0azkchf0f7oD8/2ivj2F81opcIw0tcsy/D/5Vg== +"@angular/core@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-12.2.14.tgz#6ae14e63a88c534cd48d03184cbb084ec031bad1" + integrity sha512-dlVk7yqUHL2R/eCmM8LsWuxhEBfzg0y1zHt0UqCuFwlCoiw+IG4HFy4OlZEUw9NUEZJSv0aDv3sWqxLkvK5vvg== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/flex-layout@^11.0.0-beta.33": - version "11.0.0-beta.33" - resolved "https://registry.yarnpkg.com/@angular/flex-layout/-/flex-layout-11.0.0-beta.33.tgz#c2d08f90164701b66a62b8c4365b4e22c1e95789" - integrity sha512-unfhw3abZuKtdwQicRStHCYGbANPTHYg4WNRQk/RC5Mxq+4WOp4Q8HI7GqRHCGUYDCGUP7w1sU/oDt8f09nM8w== +"@angular/flex-layout@^12.0.0-beta.35": + version "12.0.0-beta.35" + resolved "https://registry.yarnpkg.com/@angular/flex-layout/-/flex-layout-12.0.0-beta.35.tgz#b52c3c82608cbb92a119f8dcde2a5b98186fe558" + integrity sha512-nPi2MGDFuCacwWHqxF/G7lUJd2X99HbLjjUvKXnyLwyCIVgH1sfS52su2wYbVYWJRqAVAB2/VMlrtW8Khr8hDA== dependencies: - tslib "^2.0.0" + tslib "^2.1.0" -"@angular/forms@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.14.tgz#dc858408f7647f4fd033996a03aa74df18a02079" - integrity sha512-4LWqY6KEIk1AZQFnk+4PJSOCamlD4tumuVN06gO4D0dZo9Cx+GcvW6pM6N0CPubRvPs3sScCnu20WT11HNWC1w== +"@angular/forms@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-12.2.14.tgz#cb88c1c1432e3fc75d6832dfa4e97c9667396586" + integrity sha512-/9/gSJUBXVRVdRnzgJnALAQZYJATuGDMkFC9ms9DEMG4PMAhe9x4if1lJjN6noz5RAom3qNuVBNWaYAPUxlcBQ== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/language-service@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-11.2.14.tgz#452369cdffe76ac6d07836596fb47f9e69a6d119" - integrity sha512-3+0F0X4r1WeNOV6VmaMzYnJENPVmLX2/MX3/lugwZPNYKVXl/oGyh/4PB8ktntIj0tnxQuErzqRSeucNStNGRw== +"@angular/language-service@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-12.2.14.tgz#4f0afc2fe7fed6838c640a0da8658767c8372e1d" + integrity sha512-DSEhiBM077z7Gx99KQOQmtJSIdLT39GAAJ30nHO3IuBT7n5Wi+K6b/eO3mOvXrzcQ8K5WFxJboJ2GJRlPXzlwQ== -"@angular/material@^11.2.13": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-11.2.13.tgz#99960316d3ce58aac7497d7bb8b0c05468f502b9" - integrity sha512-FqFdGSkOtqsmeLyTSousodDGUy2NqbtxCIKv2rwbsIRwHNKB0KpR/UQhA2gMRuGa5hxhMJ0DW0Tf9neMRuLCTg== +"@angular/material@^12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-12.2.13.tgz#7f92f95002a2abaa8bb115ca8f0809bdc3f7d3fc" + integrity sha512-6g2GyN4qp2D+DqY2AwrQuPB3cd9gybvQVXvNRbTPXEulHr+LgGei00ySdFHFp6RvdGSMZ4i3LM1Fq3VkFxhCfQ== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/platform-browser-dynamic@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.14.tgz#3c7fff1a1daacba5390acf033d28c377ec281166" - integrity sha512-TWTPdFs6iBBcp+/YMsgCRQwdHpWGq8KjeJDJ2tfatGgBD3Gqt2YaHOMST1zPW6RkrmupytTejuVqXzeaKWFxuw== +"@angular/platform-browser-dynamic@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-12.2.14.tgz#202fd89ba7f758ca998ee5b6498d4a834472f664" + integrity sha512-0NPF7mS91Tct8rBmOLZPmnLSuS4kbLHXo6eTgrg80OC0vlzBiQwGDVW4X3KncCoX9CpevaGJCdSMc+uPNsFOUQ== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/platform-browser@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.14.tgz#e52ad7a54a42a865033a17e57e213013919f1b1d" - integrity sha512-fb7b7ss/gRoP8wLAN17W62leMgjynuyjEPU2eUoAAazsG9f2cgM+z3rK29GYncDVyYQxZUZYnjSqvL6GSXx86A== +"@angular/platform-browser@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-12.2.14.tgz#2c00d5f341827dc1de4ade98abcf329f0824790c" + integrity sha512-fWcE2rnJ3ZCISa1oPfsIDV7FBZBoLFEdDuMXAiDYqDPKvF/E5U5nHrS+K4SlLAi094bMobtTOReNWl/Ienniyw== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@angular/router@^11.2.14": - version "11.2.14" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.14.tgz#4a8eb6d010a1a45c8e7f0c3b5b705959bdb69294" - integrity sha512-3aYBmj+zrEL9yf/ntIQxHIYaWShZOBKP3U07X2mX+TPMpGlvHDnR7L6bWhQVZwewzMMz7YVR16ldg50IFuAlfA== +"@angular/router@^12.2.13": + version "12.2.14" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-12.2.14.tgz#93f965d2e50bb4ecf5a17ff5217cef266fb67c06" + integrity sha512-yP5grSnqBvc4vNhtYdcxDgDYIebUKs5f0xyFkUJM5030UnQ0CV45tBsSxHMkQbPZucIfOuxpRy8xy5+4GizuwQ== dependencies: - tslib "^2.0.0" + tslib "^2.2.0" -"@ant-design/css-animation@^1.7.2": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.3.tgz#60a1c970014e86b28f940510d69e503e428f1136" - integrity sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA== +"@assemblyscript/loader@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" + integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== "@auth0/angular-jwt@^5.0.2": version "5.0.2" @@ -297,82 +297,53 @@ dependencies: tslib "^2.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== - dependencies: - "@babel/highlight" "^7.10.4" - -"@babel/code-frame@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" - integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== - dependencies: - "@babel/highlight" "^7.12.13" - -"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.15", "@babel/compat-data@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.0.tgz#a901128bce2ad02565df95e6ecbf195cf9465919" - integrity sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q== - -"@babel/core@7.12.10": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.10.tgz#b79a2e1b9f70ed3d84bbfb6d8c4ef825f606bccd" - integrity sha512-eTAlQKq65zHfkHZV0sIVODCPGVgoo1HdBlbSLi9CqOzuZanMv2ihzY+4paiKr1mH+XmYESMAmJ/dpZ68eN6d8w== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.10" - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helpers" "^7.12.5" - "@babel/parser" "^7.12.10" - "@babel/template" "^7.12.7" - "@babel/traverse" "^7.12.10" - "@babel/types" "^7.12.10" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.1" - json5 "^2.1.2" - lodash "^4.17.19" - semver "^5.4.1" - source-map "^0.5.0" - -"@babel/core@^7.7.5": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.11.6.tgz#3a9455dc7387ff1bac45770650bc13ba04a15651" - integrity sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.6" - "@babel/helper-module-transforms" "^7.11.0" - "@babel/helpers" "^7.10.4" - "@babel/parser" "^7.11.5" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.11.5" - "@babel/types" "^7.11.5" +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" + integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== + dependencies: + "@babel/highlight" "^7.16.0" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.14.7", "@babel/compat-data@^7.16.0": + version "7.16.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" + integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== + +"@babel/core@7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.8.tgz#20cdf7c84b5d86d83fac8710a8bc605a7ba3f010" + integrity sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/generator" "^7.14.8" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-module-transforms" "^7.14.8" + "@babel/helpers" "^7.14.8" + "@babel/parser" "^7.14.8" + "@babel/template" "^7.14.5" + "@babel/traverse" "^7.14.8" + "@babel/types" "^7.14.8" convert-source-map "^1.7.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" + gensync "^1.0.0-beta.2" json5 "^2.1.2" - lodash "^4.17.19" - resolve "^1.3.2" - semver "^5.4.1" + semver "^6.3.0" source-map "^0.5.0" -"@babel/core@^7.8.6": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.3.tgz#5395e30405f0776067fbd9cf0884f15bfb770a38" - integrity sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.14.3" - "@babel/helper-compilation-targets" "^7.13.16" - "@babel/helper-module-transforms" "^7.14.2" - "@babel/helpers" "^7.14.0" - "@babel/parser" "^7.14.3" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.14.2" - "@babel/types" "^7.14.2" +"@babel/core@^7.7.5", "@babel/core@^7.8.6": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.16.0.tgz#c4ff44046f5fe310525cc9eb4ef5147f0c5374d4" + integrity sha512-mYZEvshBRHGsIAiyH5PzCFTCfbWfoYbO/jcSdXQSUQu1/pW0xDZAUP7KEc32heqWTAfAHhV9j1vH8Sav7l+JNQ== + dependencies: + "@babel/code-frame" "^7.16.0" + "@babel/generator" "^7.16.0" + "@babel/helper-compilation-targets" "^7.16.0" + "@babel/helper-module-transforms" "^7.16.0" + "@babel/helpers" "^7.16.0" + "@babel/parser" "^7.16.0" + "@babel/template" "^7.16.0" + "@babel/traverse" "^7.16.0" + "@babel/types" "^7.16.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -380,495 +351,411 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af" - integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA== +"@babel/generator@7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.8.tgz#bf86fd6af96cf3b74395a8ca409515f89423e070" + integrity sha512-cYDUpvIzhBVnMzRoY1fkSEhK/HmwEVwlyULYgn/tMQYd6Obag3ylCjONle3gdErfXBW61SVTlR9QR7uWlgeIkg== dependencies: - "@babel/types" "^7.12.11" + "@babel/types" "^7.14.8" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.11.5", "@babel/generator@^7.11.6": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" - integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== +"@babel/generator@^7.14.8", "@babel/generator@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2" + integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew== dependencies: - "@babel/types" "^7.11.5" + "@babel/types" "^7.16.0" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.12.10", "@babel/generator@^7.14.2", "@babel/generator@^7.14.3": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.3.tgz#0c2652d91f7bddab7cccc6ba8157e4f40dcedb91" - integrity sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA== +"@babel/helper-annotate-as-pure@7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz#7bf478ec3b71726d56a8ca5775b046fc29879e61" + integrity sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA== dependencies: - "@babel/types" "^7.14.2" - jsesc "^2.5.1" - source-map "^0.5.0" + "@babel/types" "^7.14.5" -"@babel/helper-annotate-as-pure@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3" - integrity sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA== +"@babel/helper-annotate-as-pure@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d" + integrity sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.16.0" -"@babel/helper-annotate-as-pure@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" - integrity sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.12.13.tgz#6bc20361c88b0a74d05137a65cac8d3cbf6f61fc" - integrity sha512-CZOv9tGphhDRlVjVkAgm8Nhklm9RzSmWpX2my+t7Ua/KT616pEzXsQCjinzvkRvHWJ9itO4f296efroX23XCMA== +"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.0.tgz#f1a686b92da794020c26582eb852e9accd0d7882" + integrity sha512-9KuleLT0e77wFUku6TUkqZzCEymBdtuQQ27MhEKzf9UOOJu3cYj98kyaDAzxpC7lV6DGiZFuC8XqDsq8/Kl6aQ== dependencies: - "@babel/helper-explode-assignable-expression" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/helper-explode-assignable-expression" "^7.16.0" + "@babel/types" "^7.16.0" -"@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.16": - version "7.13.16" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz#6e91dccf15e3f43e5556dffe32d860109887563c" - integrity sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA== +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.14.5", "@babel/helper-compilation-targets@^7.16.0": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.3.tgz#5b480cd13f68363df6ec4dc8ac8e2da11363cbf0" + integrity sha512-vKsoSQAyBmxS35JUOOt+07cLc6Nk/2ljLIHwmq2/NM6hdioUaqEXq/S+nXvbvXbZkNDlWOymPanJGOc4CBjSJA== dependencies: - "@babel/compat-data" "^7.13.15" - "@babel/helper-validator-option" "^7.12.17" - browserslist "^4.14.5" + "@babel/compat-data" "^7.16.0" + "@babel/helper-validator-option" "^7.14.5" + browserslist "^4.17.5" semver "^6.3.0" -"@babel/helper-create-class-features-plugin@^7.13.0": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.3.tgz#832111bcf4f57ca57a4c5b1a000fc125abc6554a" - integrity sha512-BnEfi5+6J2Lte9LeiL6TxLWdIlEv9Woacc1qXzXBgbikcOzMRM2Oya5XGg/f/ngotv1ej2A/b+3iJH8wbS1+lQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-member-expression-to-functions" "^7.13.12" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/helper-replace-supers" "^7.14.3" - "@babel/helper-split-export-declaration" "^7.12.13" - -"@babel/helper-create-regexp-features-plugin@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.10.4.tgz#fdd60d88524659a0b6959c0579925e425714f3b8" - integrity sha512-2/hu58IEPKeoLF45DBwx3XFqsbCXmkdAay4spVr2x0jYgRxrSNp+ePwvSsy9g6YSaNDcKIQVPXk1Ov8S2edk2g== +"@babel/helper-create-class-features-plugin@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.0.tgz#090d4d166b342a03a9fec37ef4fd5aeb9c7c6a4b" + integrity sha512-XLwWvqEaq19zFlF5PTgOod4bUA+XbkR4WLQBct1bkzmxJGB0ZEJaoKF4c8cgH9oBtCDuYJ8BP5NB9uFiEgO5QA== dependencies: - "@babel/helper-annotate-as-pure" "^7.10.4" - "@babel/helper-regex" "^7.10.4" - regexpu-core "^4.7.0" + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-function-name" "^7.16.0" + "@babel/helper-member-expression-to-functions" "^7.16.0" + "@babel/helper-optimise-call-expression" "^7.16.0" + "@babel/helper-replace-supers" "^7.16.0" + "@babel/helper-split-export-declaration" "^7.16.0" -"@babel/helper-create-regexp-features-plugin@^7.12.13": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.3.tgz#149aa6d78c016e318c43e2409a0ae9c136a86688" - integrity sha512-JIB2+XJrb7v3zceV2XzDhGIB902CmKGSpSl4q2C6agU9SNLG/2V1RtFRGPG1Ajh9STj3+q6zJMOC+N/pp2P9DA== +"@babel/helper-create-regexp-features-plugin@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.0.tgz#06b2348ce37fccc4f5e18dcd8d75053f2a7c44ff" + integrity sha512-3DyG0zAFAZKcOp7aVr33ddwkxJ0Z0Jr5V99y3I690eYLpukJsJvAbzTy1ewoCqsML8SbIrjH14Jc/nSQ4TvNPA== dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" + "@babel/helper-annotate-as-pure" "^7.16.0" regexpu-core "^4.7.1" -"@babel/helper-explode-assignable-expression@^7.12.13": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz#17b5c59ff473d9f956f40ef570cf3a76ca12657f" - integrity sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA== - dependencies: - "@babel/types" "^7.13.0" - -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-function-name@^7.12.13", "@babel/helper-function-name@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz#397688b590760b6ef7725b5f0860c82427ebaac2" - integrity sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ== - dependencies: - "@babel/helper-get-function-arity" "^7.12.13" - "@babel/template" "^7.12.13" - "@babel/types" "^7.14.2" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" - integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-hoist-variables@^7.13.0": - version "7.13.16" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.16.tgz#1b1651249e94b51f8f0d33439843e33e39775b30" - integrity sha512-1eMtTrXtrwscjcAeO4BVK+vvkxaLJSPFz1w1KLawz6HLNi9bPFGBNwwDyVfiu1Tv/vRRFYfoGaKhmAQPGPn5Wg== - dependencies: - "@babel/traverse" "^7.13.15" - "@babel/types" "^7.13.16" - -"@babel/helper-member-expression-to-functions@^7.10.4": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df" - integrity sha512-JbFlKHFntRV5qKw3YC0CvQnDZ4XMwgzzBbld7Ly4Mj4cbFy3KywcR8NtNctRToMWJOVvLINJv525Gd6wwVEx/Q== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-member-expression-to-functions@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72" - integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw== - dependencies: - "@babel/types" "^7.13.12" - -"@babel/helper-module-imports@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz#4c5c54be04bd31670a7382797d75b9fa2e5b5620" - integrity sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5", "@babel/helper-module-imports@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" - integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA== - dependencies: - "@babel/types" "^7.13.12" - -"@babel/helper-module-transforms@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.11.0.tgz#b16f250229e47211abdd84b34b64737c2ab2d359" - integrity sha512-02EVu8COMuTRO1TAzdMtpBPbe6aQ1w/8fePD2YgQmxZU4gpNWaL9gK3Jp7dxlkUlUCJOTaSeA+Hrm1BRQwqIhg== - dependencies: - "@babel/helper-module-imports" "^7.10.4" - "@babel/helper-replace-supers" "^7.10.4" - "@babel/helper-simple-access" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/template" "^7.10.4" - "@babel/types" "^7.11.0" - lodash "^4.17.19" - -"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0", "@babel/helper-module-transforms@^7.14.0", "@babel/helper-module-transforms@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz#ac1cc30ee47b945e3e0c4db12fa0c5389509dfe5" - integrity sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA== - dependencies: - "@babel/helper-module-imports" "^7.13.12" - "@babel/helper-replace-supers" "^7.13.12" - "@babel/helper-simple-access" "^7.13.12" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/helper-validator-identifier" "^7.14.0" - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.14.2" - "@babel/types" "^7.14.2" - -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-optimise-call-expression@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" - integrity sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA== - dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== - -"@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" - integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== - -"@babel/helper-regex@^7.10.4": - version "7.10.5" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.10.5.tgz#32dfbb79899073c415557053a19bd055aae50ae0" - integrity sha512-68kdUAzDrljqBrio7DYAEgCoJHxppJOERHOgOrDN7WjOzP0ZQ1LsSDRXcemzVZaLvjaJsJEESb6qt+znNuENDg== - dependencies: - lodash "^4.17.19" - -"@babel/helper-remap-async-to-generator@^7.12.1", "@babel/helper-remap-async-to-generator@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz#376a760d9f7b4b2077a9dd05aa9c3927cadb2209" - integrity sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-wrap-function" "^7.13.0" - "@babel/types" "^7.13.0" - -"@babel/helper-replace-supers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz#d585cd9388ea06e6031e4cd44b6713cbead9e6cf" - integrity sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.10.4" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.12", "@babel/helper-replace-supers@^7.14.3": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz#ca17b318b859d107f0e9b722d58cf12d94436600" - integrity sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.13.12" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/traverse" "^7.14.2" - "@babel/types" "^7.14.2" - -"@babel/helper-simple-access@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz#0f5ccda2945277a2a7a2d3a821e15395edcf3461" - integrity sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw== - dependencies: - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-simple-access@^7.13.12": - version "7.13.12" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" - integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA== - dependencies: - "@babel/types" "^7.13.12" - -"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.12.1.tgz#462dc63a7e435ade8468385c63d2b84cce4b3cbf" - integrity sha512-Mf5AUuhG1/OCChOJ/HcADmvcHM42WJockombn8ATJG3OnyiSxBK/Mm5x78BQWvmtXZKHgbjdGL2kin/HOLlZGA== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - -"@babel/helper-split-export-declaration@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" - integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== +"@babel/helper-define-polyfill-provider@^0.2.2", "@babel/helper-define-polyfill-provider@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.4.tgz#8867aed79d3ea6cade40f801efb7ac5c66916b10" + integrity sha512-OrpPZ97s+aPi6h2n1OXzdhVis1SGSsMU2aMHgLcOKfsp4/v1NWpx3CWT3lBj5eeBq9cDkPkh+YCfdF7O12uNDQ== dependencies: - "@babel/types" "^7.12.13" - -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== - -"@babel/helper-validator-identifier@^7.12.11", "@babel/helper-validator-identifier@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" - integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== - -"@babel/helper-validator-option@^7.12.11", "@babel/helper-validator-option@^7.12.17": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" - integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== - -"@babel/helper-wrap-function@^7.13.0": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz#bdb5c66fda8526ec235ab894ad53a1235c79fcc4" - integrity sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA== - dependencies: - "@babel/helper-function-name" "^7.12.13" - "@babel/template" "^7.12.13" + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-explode-assignable-expression@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz#753017337a15f46f9c09f674cff10cee9b9d7778" + integrity sha512-Hk2SLxC9ZbcOhLpg/yMznzJ11W++lg5GMbxt1ev6TXUiJB0N42KPC+7w8a+eWGuqDnUYuwStJoZHM7RgmIOaGQ== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-function-name@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.0.tgz#b7dd0797d00bbfee4f07e9c4ea5b0e30c8bb1481" + integrity sha512-BZh4mEk1xi2h4HFjWUXRQX5AEx4rvaZxHgax9gcjdLWdkjsY7MKt5p0otjsg5noXw+pB+clMCjw+aEVYADMjog== + dependencies: + "@babel/helper-get-function-arity" "^7.16.0" + "@babel/template" "^7.16.0" + "@babel/types" "^7.16.0" + +"@babel/helper-get-function-arity@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz#0088c7486b29a9cb5d948b1a1de46db66e089cfa" + integrity sha512-ASCquNcywC1NkYh/z7Cgp3w31YW8aojjYIlNg4VeJiHkqyP4AzIvr4qx7pYDb4/s8YcsZWqqOSxgkvjUz1kpDQ== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-hoist-variables@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a" + integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-member-expression-to-functions@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.0.tgz#29287040efd197c77636ef75188e81da8bccd5a4" + integrity sha512-bsjlBFPuWT6IWhl28EdrQ+gTvSvj5tqVP5Xeftp07SEuz5pLnsXZuDkDD3Rfcxy0IsHmbZ+7B2/9SHzxO0T+sQ== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.14.5", "@babel/helper-module-imports@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" + integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-module-transforms@^7.14.8", "@babel/helper-module-transforms@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.16.0.tgz#1c82a8dd4cb34577502ebd2909699b194c3e9bb5" + integrity sha512-My4cr9ATcaBbmaEa8M0dZNA74cfI6gitvUAskgDtAFmAqyFKDSHQo5YstxPbN+lzHl2D9l/YOEFqb2mtUh4gfA== + dependencies: + "@babel/helper-module-imports" "^7.16.0" + "@babel/helper-replace-supers" "^7.16.0" + "@babel/helper-simple-access" "^7.16.0" + "@babel/helper-split-export-declaration" "^7.16.0" + "@babel/helper-validator-identifier" "^7.15.7" + "@babel/template" "^7.16.0" + "@babel/traverse" "^7.16.0" + "@babel/types" "^7.16.0" + +"@babel/helper-optimise-call-expression@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz#cecdb145d70c54096b1564f8e9f10cd7d193b338" + integrity sha512-SuI467Gi2V8fkofm2JPnZzB/SUuXoJA5zXe/xzyPP2M04686RzFKFHPK6HDVN6JvWBIEW8tt9hPR7fXdn2Lgpw== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz#5ac822ce97eec46741ab70a517971e443a70c5a9" + integrity sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ== + +"@babel/helper-remap-async-to-generator@^7.14.5", "@babel/helper-remap-async-to-generator@^7.16.0", "@babel/helper-remap-async-to-generator@^7.16.4": + version "7.16.4" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.4.tgz#5d7902f61349ff6b963e07f06a389ce139fbfe6e" + integrity sha512-vGERmmhR+s7eH5Y/cp8PCVzj4XEjerq8jooMfxFdA5xVtAk9Sh4AQsrWgiErUEBjtGrBtOFKDUcWQFW4/dFwMA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-wrap-function" "^7.16.0" + "@babel/types" "^7.16.0" + +"@babel/helper-replace-supers@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.0.tgz#73055e8d3cf9bcba8ddb55cad93fedc860f68f17" + integrity sha512-TQxuQfSCdoha7cpRNJvfaYxxxzmbxXw/+6cS7V02eeDYyhxderSoMVALvwupA54/pZcOTtVeJ0xccp1nGWladA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.16.0" + "@babel/helper-optimise-call-expression" "^7.16.0" + "@babel/traverse" "^7.16.0" + "@babel/types" "^7.16.0" + +"@babel/helper-simple-access@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.0.tgz#21d6a27620e383e37534cf6c10bba019a6f90517" + integrity sha512-o1rjBT/gppAqKsYfUdfHq5Rk03lMQrkPHG1OWzHWpLgVXRH4HnMM9Et9CVdIqwkCQlobnGHEJMsgWP/jE1zUiw== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" + integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-split-export-declaration@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.0.tgz#29672f43663e936df370aaeb22beddb3baec7438" + integrity sha512-0YMMRpuDFNGTHNRiiqJX19GjNXA4H0E8jZ2ibccfSxaCogbm3am5WN/2nQNj0YnQwGWM1J06GOcQ2qnh3+0paw== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-validator-identifier@^7.15.7": + version "7.15.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" + integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== + +"@babel/helper-validator-option@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" + integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== + +"@babel/helper-wrap-function@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.0.tgz#b3cf318afce774dfe75b86767cd6d68f3482e57c" + integrity sha512-VVMGzYY3vkWgCJML+qVLvGIam902mJW0FvT7Avj1zEe0Gn7D93aWdLblYARTxEw+6DhZmtzhBM2zv0ekE5zg1g== + dependencies: + "@babel/helper-function-name" "^7.16.0" + "@babel/template" "^7.16.0" + "@babel/traverse" "^7.16.0" + "@babel/types" "^7.16.0" + +"@babel/helpers@^7.14.8", "@babel/helpers@^7.16.0": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.16.3.tgz#27fc64f40b996e7074dc73128c3e5c3e7f55c43c" + integrity sha512-Xn8IhDlBPhvYTvgewPKawhADichOsbkZuzN7qz2BusOM0brChsyXMDJvldWaYMMUNiCQdQzNEioXTp3sC8Nt8w== + dependencies: + "@babel/template" "^7.16.0" + "@babel/traverse" "^7.16.3" + "@babel/types" "^7.16.0" + +"@babel/highlight@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.0.tgz#6ceb32b2ca4b8f5f361fb7fd821e3fddf4a1725a" + integrity sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g== + dependencies: + "@babel/helper-validator-identifier" "^7.15.7" + chalk "^2.0.0" + js-tokens "^4.0.0" -"@babel/helpers@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.4.tgz#2abeb0d721aff7c0a97376b9e1f6f65d7a475044" - integrity sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA== - dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.10.4" - "@babel/types" "^7.10.4" +"@babel/parser@^7.14.5", "@babel/parser@^7.14.8", "@babel/parser@^7.16.0", "@babel/parser@^7.16.3": + version "7.16.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e" + integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng== -"@babel/helpers@^7.12.5", "@babel/helpers@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.0.tgz#ea9b6be9478a13d6f961dbb5f36bf75e2f3b8f62" - integrity sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.0.tgz#358972eaab006f5eb0826183b0c93cbcaf13e1e2" + integrity sha512-4tcFwwicpWTrpl9qjf7UsoosaArgImF85AxqCRZlgc3IQDvkUHjJpruXAL58Wmj+T6fypWTC/BakfEkwIL/pwA== dependencies: - "@babel/template" "^7.12.13" - "@babel/traverse" "^7.14.0" - "@babel/types" "^7.14.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-proposal-optional-chaining" "^7.16.0" -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== +"@babel/plugin-proposal-async-generator-functions@7.14.7": + version "7.14.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz#784a48c3d8ed073f65adcf30b57bcbf6c8119ace" + integrity sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - chalk "^2.0.0" - js-tokens "^4.0.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/highlight@^7.12.13": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" - integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== +"@babel/plugin-proposal-async-generator-functions@^7.14.7": + version "7.16.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.4.tgz#e606eb6015fec6fa5978c940f315eae4e300b081" + integrity sha512-/CUekqaAaZCQHleSK/9HajvcD/zdnJiKRiuUFq8ITE+0HsPzquf53cpFiqAwl/UfmJbR6n5uGPQSPdrmKOvHHg== dependencies: - "@babel/helper-validator-identifier" "^7.14.0" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.10.4", "@babel/parser@^7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" - integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== - -"@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.14.2", "@babel/parser@^7.14.3": - version "7.14.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.3.tgz#9b530eecb071fd0c93519df25c5ff9f14759f298" - integrity sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ== + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.16.4" + "@babel/plugin-syntax-async-generators" "^7.8.4" -"@babel/plugin-proposal-async-generator-functions@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.2.tgz#3a2085abbf5d5f962d480dbc81347385ed62eb1e" - integrity sha512-b1AM4F6fwck4N8ItZ/AtC4FP/cqZqmKRQ4FaTDutwSYyjuhtvsGEMLK4N/ztV/ImP40BjIDyMgBQAeAMsQYVFQ== +"@babel/plugin-proposal-class-properties@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.0.tgz#c029618267ddebc7280fa286e0f8ca2a278a2d1a" + integrity sha512-mCF3HcuZSY9Fcx56Lbn+CGdT44ioBMMvjNVldpKtj8tpniETdLjnxdHI1+sDWXIM1nNt+EanJOZ3IG9lzVjs7A== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-remap-async-to-generator" "^7.13.0" - "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/helper-create-class-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-class-properties@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz#146376000b94efd001e57a40a88a525afaab9f37" - integrity sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg== +"@babel/plugin-proposal-class-static-block@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.0.tgz#5296942c564d8144c83eea347d0aa8a0b89170e7" + integrity sha512-mAy3sdcY9sKAkf3lQbDiv3olOfiLqI51c9DR9b19uMoR2Z6r5pmGl7dfNFqEvqOyqbf1ta4lknK4gc5PJn3mfA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-create-class-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-proposal-dynamic-import@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.2.tgz#01ebabd7c381cff231fa43e302939a9de5be9d9f" - integrity sha512-oxVQZIWFh91vuNEMKltqNsKLFWkOIyJc95k2Gv9lWVyDfPUQGSSlbDEgWuJUU1afGE9WwlzpucMZ3yDRHIItkA== +"@babel/plugin-proposal-dynamic-import@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.0.tgz#783eca61d50526202f9b296095453977e88659f1" + integrity sha512-QGSA6ExWk95jFQgwz5GQ2Dr95cf7eI7TKutIXXTb7B1gCLTCz5hTjFTQGfLFBBiC5WSNi7udNwWsqbbMh1c4yQ== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" -"@babel/plugin-proposal-export-namespace-from@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.2.tgz#62542f94aa9ce8f6dba79eec698af22112253791" - integrity sha512-sRxW3z3Zp3pFfLAgVEvzTFutTXax837oOatUIvSG9o5gRj9mKwm3br1Se5f4QalTQs9x4AzlA/HrCWbQIHASUQ== +"@babel/plugin-proposal-export-namespace-from@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.0.tgz#9c01dee40b9d6b847b656aaf4a3976a71740f222" + integrity sha512-CjI4nxM/D+5wCnhD11MHB1AwRSAYeDT+h8gCdcVJZ/OK7+wRzFsf7PFPWVpVpNRkHMmMkQWAHpTq+15IXQ1diA== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" -"@babel/plugin-proposal-json-strings@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.2.tgz#830b4e2426a782e8b2878fbfe2cba85b70cbf98c" - integrity sha512-w2DtsfXBBJddJacXMBhElGEYqCZQqN99Se1qeYn8DVLB33owlrlLftIbMzn5nz1OITfDVknXF433tBrLEAOEjA== +"@babel/plugin-proposal-json-strings@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.0.tgz#cae35a95ed1d2a7fa29c4dc41540b84a72e9ab25" + integrity sha512-kouIPuiv8mSi5JkEhzApg5Gn6hFyKPnlkO0a9YSzqRurH8wYzSlf6RJdzluAsbqecdW5pBvDJDfyDIUR/vLxvg== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-json-strings" "^7.8.3" -"@babel/plugin-proposal-logical-assignment-operators@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.2.tgz#222348c080a1678e0e74ea63fe76f275882d1fd7" - integrity sha512-1JAZtUrqYyGsS7IDmFeaem+/LJqujfLZ2weLR9ugB0ufUPjzf8cguyVT1g5im7f7RXxuLq1xUxEzvm68uYRtGg== +"@babel/plugin-proposal-logical-assignment-operators@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.0.tgz#a711b8ceb3ffddd3ef88d3a49e86dbd3cc7db3fd" + integrity sha512-pbW0fE30sVTYXXm9lpVQQ/Vc+iTeQKiXlaNRZPPN2A2VdlWyAtsUrsQ3xydSlDW00TFMK7a8m3cDTkBF5WnV3Q== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" -"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.2.tgz#425b11dc62fc26939a2ab42cbba680bdf5734546" - integrity sha512-ebR0zU9OvI2N4qiAC38KIAK75KItpIPTpAtd2r4OZmMFeKbKJpUFLYP2EuDut82+BmYi8sz42B+TfTptJ9iG5Q== +"@babel/plugin-proposal-nullish-coalescing-operator@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.0.tgz#44e1cce08fe2427482cf446a91bb451528ed0596" + integrity sha512-3bnHA8CAFm7cG93v8loghDYyQ8r97Qydf63BeYiGgYbjKKB/XP53W15wfRC7dvKfoiJ34f6Rbyyx2btExc8XsQ== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" -"@babel/plugin-proposal-numeric-separator@^7.12.7": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.2.tgz#82b4cc06571143faf50626104b335dd71baa4f9e" - integrity sha512-DcTQY9syxu9BpU3Uo94fjCB3LN9/hgPS8oUL7KrSW3bA2ePrKZZPJcc5y0hoJAM9dft3pGfErtEUvxXQcfLxUg== +"@babel/plugin-proposal-numeric-separator@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.0.tgz#5d418e4fbbf8b9b7d03125d3a52730433a373734" + integrity sha512-FAhE2I6mjispy+vwwd6xWPyEx3NYFS13pikDBWUAFGZvq6POGs5eNchw8+1CYoEgBl9n11I3NkzD7ghn25PQ9Q== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" -"@babel/plugin-proposal-object-rest-spread@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.2.tgz#e17d418f81cc103fedd4ce037e181c8056225abc" - integrity sha512-hBIQFxwZi8GIp934+nj5uV31mqclC1aYDhctDu5khTi9PCCUOczyy0b34W0oE9U/eJXiqQaKyVsmjeagOaSlbw== +"@babel/plugin-proposal-object-rest-spread@^7.14.7": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.0.tgz#5fb32f6d924d6e6712810362a60e12a2609872e6" + integrity sha512-LU/+jp89efe5HuWJLmMmFG0+xbz+I2rSI7iLc1AlaeSMDMOGzWlc5yJrMN1d04osXN4sSfpo4O+azkBNBes0jg== dependencies: - "@babel/compat-data" "^7.14.0" - "@babel/helper-compilation-targets" "^7.13.16" - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/compat-data" "^7.16.0" + "@babel/helper-compilation-targets" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.14.2" + "@babel/plugin-transform-parameters" "^7.16.0" -"@babel/plugin-proposal-optional-catch-binding@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.2.tgz#150d4e58e525b16a9a1431bd5326c4eed870d717" - integrity sha512-XtkJsmJtBaUbOxZsNk0Fvrv8eiqgneug0A6aqLFZ4TSkar2L5dSXWcnUKHgmjJt49pyB/6ZHvkr3dPgl9MOWRQ== +"@babel/plugin-proposal-optional-catch-binding@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.0.tgz#5910085811ab4c28b00d6ebffa4ab0274d1e5f16" + integrity sha512-kicDo0A/5J0nrsCPbn89mTG3Bm4XgYi0CZtvex9Oyw7gGZE3HXGD0zpQNH+mo+tEfbo8wbmMvJftOwpmPy7aVw== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" -"@babel/plugin-proposal-optional-chaining@^7.12.7": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.2.tgz#df8171a8b9c43ebf4c1dabe6311b432d83e1b34e" - integrity sha512-qQByMRPwMZJainfig10BoaDldx/+VDtNcrA7qdNaEOAj6VXud+gfrkA8j4CRAU5HjnWREXqIpSpH30qZX1xivA== +"@babel/plugin-proposal-optional-chaining@^7.14.5", "@babel/plugin-proposal-optional-chaining@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.0.tgz#56dbc3970825683608e9efb55ea82c2a2d6c8dc0" + integrity sha512-Y4rFpkZODfHrVo70Uaj6cC1JJOt3Pp0MdWSwIKtb8z1/lsjl9AmnB7ErRFV+QNGIfcY1Eruc2UMx5KaRnXjMyg== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" "@babel/plugin-syntax-optional-chaining" "^7.8.3" -"@babel/plugin-proposal-private-methods@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz#04bd4c6d40f6e6bbfa2f57e2d8094bad900ef787" - integrity sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q== +"@babel/plugin-proposal-private-methods@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.0.tgz#b4dafb9c717e4301c5776b30d080d6383c89aff6" + integrity sha512-IvHmcTHDFztQGnn6aWq4t12QaBXTKr1whF/dgp9kz84X6GUcwq9utj7z2wFCUfeOup/QKnOlt2k0zxkGFx9ubg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-create-class-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-proposal-unicode-property-regex@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz#bebde51339be829c17aaaaced18641deb62b39ba" - integrity sha512-XyJmZidNfofEkqFV5VC/bLabGmO5QzenPO/YOfGuEbgU+2sSwMmio3YLb4WtBgcmmdwZHyVyv8on77IUjQ5Gvg== +"@babel/plugin-proposal-private-property-in-object@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.0.tgz#69e935b2c5c79d2488112d886f0c4e2790fee76f" + integrity sha512-3jQUr/HBbMVZmi72LpjQwlZ55i1queL8KcDTQEkAHihttJnAPrcvG9ZNXIfsd2ugpizZo595egYV6xy+pv4Ofw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-create-class-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" -"@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.10.4.tgz#4483cda53041ce3413b7fe2f00022665ddfaa75d" - integrity sha512-H+3fOgPnEXFL9zGYtKQe4IDOPKYlZdF1kqFDQRRb8PK4B8af1vAGK04tF5iQAAsui+mHNBQSAtd2/ndEDe9wuA== +"@babel/plugin-proposal-unicode-property-regex@^7.14.5", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.0.tgz#890482dfc5ea378e42e19a71e709728cabf18612" + integrity sha512-ti7IdM54NXv29cA4+bNNKEMS4jLMCbJgl+Drv+FgYy0erJLAxNAIXcNjNjrRZEcWq0xJHsNVwQezskMFpF8N9g== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4": +"@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-class-properties@^7.12.1": +"@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: "@babel/helper-plugin-utils" "^7.12.13" -"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== @@ -882,7 +769,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3": +"@babel/plugin-syntax-json-strings@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== @@ -896,7 +783,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== @@ -910,384 +797,393 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" -"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3": +"@babel/plugin-syntax-object-rest-spread@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3": +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3": +"@babel/plugin-syntax-optional-chaining@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-top-level-await@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz#c5f0fa6e249f5b739727f923540cf7a806130178" - integrity sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ== +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-arrow-functions@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz#10a59bebad52d637a027afa692e8d5ceff5e3dae" - integrity sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg== +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-async-to-generator@7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz#3849a49cc2a22e9743cbd6b52926d30337229af1" - integrity sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A== +"@babel/plugin-transform-arrow-functions@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.0.tgz#951706f8b449c834ed07bd474c0924c944b95a8e" + integrity sha512-vIFb5250Rbh7roWARvCLvIJ/PtAU5Lhv7BtZ1u24COwpI9Ypjsh+bZcKk6rlIyalK+r0jOc1XQ8I4ovNxNrWrA== dependencies: - "@babel/helper-module-imports" "^7.12.1" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-remap-async-to-generator" "^7.12.1" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-async-to-generator@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz#8e112bf6771b82bf1e974e5e26806c5c99aa516f" - integrity sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg== +"@babel/plugin-transform-async-to-generator@7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz#72c789084d8f2094acb945633943ef8443d39e67" + integrity sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA== dependencies: - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-remap-async-to-generator" "^7.13.0" + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.14.5" -"@babel/plugin-transform-block-scoped-functions@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz#a9bf1836f2a39b4eb6cf09967739de29ea4bf4c4" - integrity sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg== +"@babel/plugin-transform-async-to-generator@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.0.tgz#df12637f9630ddfa0ef9d7a11bc414d629d38604" + integrity sha512-PbIr7G9kR8tdH6g8Wouir5uVjklETk91GMVSUq+VaOgiinbCkBP6Q7NN/suM/QutZkMJMvcyAriogcYAdhg8Gw== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-module-imports" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-remap-async-to-generator" "^7.16.0" -"@babel/plugin-transform-block-scoping@^7.12.11": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.14.2.tgz#761cb12ab5a88d640ad4af4aa81f820e6b5fdf5c" - integrity sha512-neZZcP19NugZZqNwMTH+KoBjx5WyvESPSIOQb4JHpfd+zPfqcH65RMu5xJju5+6q/Y2VzYrleQTr+b6METyyxg== +"@babel/plugin-transform-block-scoped-functions@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.0.tgz#c618763233ad02847805abcac4c345ce9de7145d" + integrity sha512-V14As3haUOP4ZWrLJ3VVx5rCnrYhMSHN/jX7z6FAt5hjRkLsb0snPCmJwSOML5oxkKO4FNoNv7V5hw/y2bjuvg== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-classes@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.2.tgz#3f1196c5709f064c252ad056207d87b7aeb2d03d" - integrity sha512-7oafAVcucHquA/VZCsXv/gmuiHeYd64UJyyTYU+MPfNu0KeNlxw06IeENBO8bJjXVbolu+j1MM5aKQtH1OMCNg== +"@babel/plugin-transform-block-scoping@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.0.tgz#bcf433fb482fe8c3d3b4e8a66b1c4a8e77d37c16" + integrity sha512-27n3l67/R3UrXfizlvHGuTwsRIFyce3D/6a37GRxn28iyTPvNXaW4XvznexRh1zUNLPjbLL22Id0XQElV94ruw== dependencies: - "@babel/helper-annotate-as-pure" "^7.12.13" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-replace-supers" "^7.13.12" - "@babel/helper-split-export-declaration" "^7.12.13" - globals "^11.1.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-computed-properties@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz#845c6e8b9bb55376b1fa0b92ef0bdc8ea06644ed" - integrity sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg== +"@babel/plugin-transform-classes@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.0.tgz#54cf5ff0b2242c6573d753cd4bfc7077a8b282f5" + integrity sha512-HUxMvy6GtAdd+GKBNYDWCIA776byUQH8zjnfjxwT1P1ARv/wFu8eBDpmXQcLS/IwRtrxIReGiplOwMeyO7nsDQ== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-function-name" "^7.16.0" + "@babel/helper-optimise-call-expression" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.16.0" + "@babel/helper-split-export-declaration" "^7.16.0" + globals "^11.1.0" -"@babel/plugin-transform-destructuring@^7.12.1": - version "7.13.17" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.17.tgz#678d96576638c19d5b36b332504d3fd6e06dea27" - integrity sha512-UAUqiLv+uRLO+xuBKKMEpC+t7YRNVRqBsWWq1yKXbBZBje/t3IXCiSinZhjn/DC3qzBfICeYd2EFGEbHsh5RLA== +"@babel/plugin-transform-computed-properties@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.0.tgz#e0c385507d21e1b0b076d66bed6d5231b85110b7" + integrity sha512-63l1dRXday6S8V3WFY5mXJwcRAnPYxvFfTlt67bwV1rTyVTM5zrp0DBBb13Kl7+ehkCVwIZPumPpFP/4u70+Tw== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-dotall-regex@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz#3f1601cc29905bfcb67f53910f197aeafebb25ad" - integrity sha512-foDrozE65ZFdUC2OfgeOCrEPTxdB3yjqxpXh8CH+ipd9CHd4s/iq81kcUpyH8ACGNEPdFqbtzfgzbT/ZGlbDeQ== +"@babel/plugin-transform-destructuring@^7.14.7": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.0.tgz#ad3d7e74584ad5ea4eadb1e6642146c590dee33c" + integrity sha512-Q7tBUwjxLTsHEoqktemHBMtb3NYwyJPTJdM+wDwb0g8PZ3kQUIzNvwD5lPaqW/p54TXBc/MXZu9Jr7tbUEUM8Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.10.4.tgz#469c2062105c1eb6a040eaf4fac4b488078395ee" - integrity sha512-ZEAVvUTCMlMFAbASYSVQoxIbHm2OkG2MseW6bV2JjIygOjdVv8tuxrCTzj1+Rynh7ODb8GivUy7dzEXzEhuPaA== +"@babel/plugin-transform-dotall-regex@^7.14.5", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.0.tgz#50bab00c1084b6162d0a58a818031cf57798e06f" + integrity sha512-FXlDZfQeLILfJlC6I1qyEwcHK5UpRCFkaoVyA1nk9A1L1Yu583YO4un2KsLBsu3IJb4CUbctZks8tD9xPQubLw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.10.4" - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-create-regexp-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-duplicate-keys@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz#6f06b87a8b803fd928e54b81c258f0a0033904de" - integrity sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ== +"@babel/plugin-transform-duplicate-keys@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.0.tgz#8bc2e21813e3e89e5e5bf3b60aa5fc458575a176" + integrity sha512-LIe2kcHKAZOJDNxujvmp6z3mfN6V9lJxubU4fJIGoQCkKe3Ec2OcbdlYP+vW++4MpxwG0d1wSDOJtQW5kLnkZQ== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-exponentiation-operator@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz#4d52390b9a273e651e4aba6aee49ef40e80cd0a1" - integrity sha512-fbUelkM1apvqez/yYx1/oICVnGo2KM5s63mhGylrmXUxK/IAXSIf87QIxVfZldWf4QsOafY6vV3bX8aMHSvNrA== +"@babel/plugin-transform-exponentiation-operator@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.0.tgz#a180cd2881e3533cef9d3901e48dad0fbeff4be4" + integrity sha512-OwYEvzFI38hXklsrbNivzpO3fh87skzx8Pnqi4LoSYeav0xHlueSoCJrSgTPfnbyzopo5b3YVAJkFIcUpK2wsw== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-for-of@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz#c799f881a8091ac26b54867a845c3e97d2696062" - integrity sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg== +"@babel/plugin-transform-for-of@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.0.tgz#f7abaced155260e2461359bbc7c7248aca5e6bd2" + integrity sha512-5QKUw2kO+GVmKr2wMYSATCTTnHyscl6sxFRAY+rvN7h7WB0lcG0o4NoV6ZQU32OZGVsYUsfLGgPQpDFdkfjlJQ== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-function-name@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz#bb024452f9aaed861d374c8e7a24252ce3a50051" - integrity sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ== +"@babel/plugin-transform-function-name@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.0.tgz#02e3699c284c6262236599f751065c5d5f1f400e" + integrity sha512-lBzMle9jcOXtSOXUpc7tvvTpENu/NuekNJVova5lCCWCV9/U1ho2HH2y0p6mBg8fPm/syEAbfaaemYGOHCY3mg== dependencies: - "@babel/helper-function-name" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-function-name" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-literals@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz#2ca45bafe4a820197cf315794a4d26560fe4bdb9" - integrity sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ== +"@babel/plugin-transform-literals@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.0.tgz#79711e670ffceb31bd298229d50f3621f7980cac" + integrity sha512-gQDlsSF1iv9RU04clgXqRjrPyyoJMTclFt3K1cjLmTKikc0s/6vE3hlDeEVC71wLTRu72Fq7650kABrdTc2wMQ== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-member-expression-literals@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz#5ffa66cd59b9e191314c9f1f803b938e8c081e40" - integrity sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg== +"@babel/plugin-transform-member-expression-literals@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.0.tgz#5251b4cce01eaf8314403d21aedb269d79f5e64b" + integrity sha512-WRpw5HL4Jhnxw8QARzRvwojp9MIE7Tdk3ez6vRyUk1MwgjJN0aNpRoXainLR5SgxmoXx/vsXGZ6OthP6t/RbUg== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-modules-amd@^7.12.1": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.2.tgz#6622806fe1a7c07a1388444222ef9535f2ca17b0" - integrity sha512-hPC6XBswt8P3G2D1tSV2HzdKvkqOpmbyoy+g73JG0qlF/qx2y3KaMmXb1fLrpmWGLZYA0ojCvaHdzFWjlmV+Pw== +"@babel/plugin-transform-modules-amd@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.0.tgz#09abd41e18dcf4fd479c598c1cef7bd39eb1337e" + integrity sha512-rWFhWbCJ9Wdmzln1NmSCqn7P0RAD+ogXG/bd9Kg5c7PKWkJtkiXmYsMBeXjDlzHpVTJ4I/hnjs45zX4dEv81xw== dependencies: - "@babel/helper-module-transforms" "^7.14.2" - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-module-transforms" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.12.1": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.0.tgz#52bc199cb581e0992edba0f0f80356467587f161" - integrity sha512-EX4QePlsTaRZQmw9BsoPeyh5OCtRGIhwfLquhxGp5e32w+dyL8htOcDwamlitmNFK6xBZYlygjdye9dbd9rUlQ== +"@babel/plugin-transform-modules-commonjs@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.0.tgz#add58e638c8ddc4875bd9a9ecb5c594613f6c922" + integrity sha512-Dzi+NWqyEotgzk/sb7kgQPJQf7AJkQBWsVp1N6JWc1lBVo0vkElUnGdr1PzUBmfsCCN5OOFya3RtpeHk15oLKQ== dependencies: - "@babel/helper-module-transforms" "^7.14.0" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-simple-access" "^7.13.12" + "@babel/helper-module-transforms" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-simple-access" "^7.16.0" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.12.1": - version "7.13.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3" - integrity sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A== +"@babel/plugin-transform-modules-systemjs@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.0.tgz#a92cf240afeb605f4ca16670453024425e421ea4" + integrity sha512-yuGBaHS3lF1m/5R+6fjIke64ii5luRUg97N2wr+z1sF0V+sNSXPxXDdEEL/iYLszsN5VKxVB1IPfEqhzVpiqvg== dependencies: - "@babel/helper-hoist-variables" "^7.13.0" - "@babel/helper-module-transforms" "^7.13.0" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-validator-identifier" "^7.12.11" + "@babel/helper-hoist-variables" "^7.16.0" + "@babel/helper-module-transforms" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-identifier" "^7.15.7" babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-umd@^7.12.1": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.0.tgz#2f8179d1bbc9263665ce4a65f305526b2ea8ac34" - integrity sha512-nPZdnWtXXeY7I87UZr9VlsWme3Y0cfFFE41Wbxz4bbaexAjNMInXPFUpRRUJ8NoMm0Cw+zxbqjdPmLhcjfazMw== +"@babel/plugin-transform-modules-umd@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.0.tgz#195f26c2ad6d6a391b70880effce18ce625e06a7" + integrity sha512-nx4f6no57himWiHhxDM5pjwhae5vLpTK2zCnDH8+wNLJy0TVER/LJRHl2bkt6w9Aad2sPD5iNNoUpY3X9sTGDg== dependencies: - "@babel/helper-module-transforms" "^7.14.0" - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-module-transforms" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-named-capturing-groups-regex@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.13.tgz#2213725a5f5bbbe364b50c3ba5998c9599c5c9d9" - integrity sha512-Xsm8P2hr5hAxyYblrfACXpQKdQbx4m2df9/ZZSQ8MAhsadw06+jW7s9zsSw6he+mJZXRlVMyEnVktJo4zjk1WA== +"@babel/plugin-transform-named-capturing-groups-regex@^7.14.7": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.0.tgz#d3db61cc5d5b97986559967cd5ea83e5c32096ca" + integrity sha512-LogN88uO+7EhxWc8WZuQ8vxdSyVGxhkh8WTC3tzlT8LccMuQdA81e9SGV6zY7kY2LjDhhDOFdQVxdGwPyBCnvg== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" + "@babel/helper-create-regexp-features-plugin" "^7.16.0" -"@babel/plugin-transform-new-target@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.13.tgz#e22d8c3af24b150dd528cbd6e685e799bf1c351c" - integrity sha512-/KY2hbLxrG5GTQ9zzZSc3xWiOy379pIETEhbtzwZcw9rvuaVV4Fqy7BYGYOWZnaoXIQYbbJ0ziXLa/sKcGCYEQ== +"@babel/plugin-transform-new-target@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.0.tgz#af823ab576f752215a49937779a41ca65825ab35" + integrity sha512-fhjrDEYv2DBsGN/P6rlqakwRwIp7rBGLPbrKxwh7oVt5NNkIhZVOY2GRV+ULLsQri1bDqwDWnU3vhlmx5B2aCw== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-object-super@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz#b4416a2d63b8f7be314f3d349bd55a9c1b5171f7" - integrity sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ== +"@babel/plugin-transform-object-super@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.0.tgz#fb20d5806dc6491a06296ac14ea8e8d6fedda72b" + integrity sha512-fds+puedQHn4cPLshoHcR1DTMN0q1V9ou0mUjm8whx9pGcNvDrVVrgw+KJzzCaiTdaYhldtrUps8DWVMgrSEyg== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - "@babel/helper-replace-supers" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-replace-supers" "^7.16.0" -"@babel/plugin-transform-parameters@^7.12.1", "@babel/plugin-transform-parameters@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.2.tgz#e4290f72e0e9e831000d066427c4667098decc31" - integrity sha512-NxoVmA3APNCC1JdMXkdYXuQS+EMdqy0vIwyDHeKHiJKRxmp1qGSdb0JLEIoPRhkx6H/8Qi3RJ3uqOCYw8giy9A== +"@babel/plugin-transform-parameters@^7.14.5", "@babel/plugin-transform-parameters@^7.16.0": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.3.tgz#fa9e4c874ee5223f891ee6fa8d737f4766d31d15" + integrity sha512-3MaDpJrOXT1MZ/WCmkOFo7EtmVVC8H4EUZVrHvFOsmwkk4lOjQj8rzv8JKUZV4YoQKeoIgk07GO+acPU9IMu/w== dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-property-literals@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz#4e6a9e37864d8f1b3bc0e2dce7bf8857db8b1a81" - integrity sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A== +"@babel/plugin-transform-property-literals@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.0.tgz#a95c552189a96a00059f6776dc4e00e3690c78d1" + integrity sha512-XLldD4V8+pOqX2hwfWhgwXzGdnDOThxaNTgqagOcpBgIxbUvpgU2FMvo5E1RyHbk756WYgdbS0T8y0Cj9FKkWQ== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-regenerator@^7.12.1": - version "7.13.15" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.13.15.tgz#e5eb28945bf8b6563e7f818945f966a8d2997f39" - integrity sha512-Bk9cOLSz8DiurcMETZ8E2YtIVJbFCPGW28DJWUakmyVWtQSm6Wsf0p4B4BfEr/eL2Nkhe/CICiUiMOCi1TPhuQ== +"@babel/plugin-transform-regenerator@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.0.tgz#eaee422c84b0232d03aea7db99c97deeaf6125a4" + integrity sha512-JAvGxgKuwS2PihiSFaDrp94XOzzTUeDeOQlcKzVAyaPap7BnZXK/lvMDiubkPTdotPKOIZq9xWXWnggUMYiExg== dependencies: regenerator-transform "^0.14.2" -"@babel/plugin-transform-reserved-words@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.13.tgz#7d9988d4f06e0fe697ea1d9803188aa18b472695" - integrity sha512-xhUPzDXxZN1QfiOy/I5tyye+TRz6lA7z6xaT4CLOjPRMVg1ldRf0LHw0TDBpYL4vG78556WuHdyO9oi5UmzZBg== +"@babel/plugin-transform-reserved-words@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.0.tgz#fff4b9dcb19e12619394bda172d14f2d04c0379c" + integrity sha512-Dgs8NNCehHSvXdhEhln8u/TtJxfVwGYCgP2OOr5Z3Ar+B+zXicEOKNTyc+eca2cuEOMtjW6m9P9ijOt8QdqWkg== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-runtime@7.12.10": - version "7.12.10" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.10.tgz#af0fded4e846c4b37078e8e5d06deac6cd848562" - integrity sha512-xOrUfzPxw7+WDm9igMgQCbO3cJKymX7dFdsgRr1eu9n3KjjyU4pptIXbXPseQDquw+W+RuJEJMHKHNsPNNm3CA== - dependencies: - "@babel/helper-module-imports" "^7.12.5" - "@babel/helper-plugin-utils" "^7.10.4" - semver "^5.5.1" - -"@babel/plugin-transform-shorthand-properties@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz#db755732b70c539d504c6390d9ce90fe64aff7ad" - integrity sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-spread@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz#84887710e273c1815ace7ae459f6f42a5d31d5fd" - integrity sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" - -"@babel/plugin-transform-sticky-regex@^7.12.7": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz#760ffd936face73f860ae646fb86ee82f3d06d1f" - integrity sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-template-literals@^7.12.1": - version "7.13.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz#a36049127977ad94438dee7443598d1cefdf409d" - integrity sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw== - dependencies: - "@babel/helper-plugin-utils" "^7.13.0" + "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-transform-typeof-symbol@^7.12.10": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz#785dd67a1f2ea579d9c2be722de8c84cb85f5a7f" - integrity sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-unicode-escapes@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz#840ced3b816d3b5127dd1d12dcedc5dead1a5e74" - integrity sha512-0bHEkdwJ/sN/ikBHfSmOXPypN/beiGqjo+o4/5K+vxEFNPRPdImhviPakMKG4x96l85emoa0Z6cDflsdBusZbw== +"@babel/plugin-transform-runtime@7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.14.5.tgz#30491dad49c6059f8f8fa5ee8896a0089e987523" + integrity sha512-fPMBhh1AV8ZyneiCIA+wYYUH1arzlXR1UMcApjvchDhfKxhy2r2lReJv8uHEyihi4IFIGlr1Pdx7S5fkESDQsg== dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-transform-unicode-regex@^7.12.1": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz#b52521685804e155b1202e83fc188d34bb70f5ac" - integrity sha512-mDRzSNY7/zopwisPZ5kM9XKCfhchqIYwAKRERtEnhYscZB79VRekuRSoYbN0+KVe3y8+q1h6A4svXtP7N+UoCA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.12.13" - "@babel/helper-plugin-utils" "^7.12.13" + "@babel/helper-module-imports" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + babel-plugin-polyfill-corejs2 "^0.2.2" + babel-plugin-polyfill-corejs3 "^0.2.2" + babel-plugin-polyfill-regenerator "^0.2.2" + semver "^6.3.0" -"@babel/preset-env@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.12.11.tgz#55d5f7981487365c93dbbc84507b1c7215e857f9" - integrity sha512-j8Tb+KKIXKYlDBQyIOy4BLxzv1NUOwlHfZ74rvW+Z0Gp4/cI2IMDPBWAgWceGcE7aep9oL/0K9mlzlMGxA8yNw== - dependencies: - "@babel/compat-data" "^7.12.7" - "@babel/helper-compilation-targets" "^7.12.5" - "@babel/helper-module-imports" "^7.12.5" - "@babel/helper-plugin-utils" "^7.10.4" - "@babel/helper-validator-option" "^7.12.11" - "@babel/plugin-proposal-async-generator-functions" "^7.12.1" - "@babel/plugin-proposal-class-properties" "^7.12.1" - "@babel/plugin-proposal-dynamic-import" "^7.12.1" - "@babel/plugin-proposal-export-namespace-from" "^7.12.1" - "@babel/plugin-proposal-json-strings" "^7.12.1" - "@babel/plugin-proposal-logical-assignment-operators" "^7.12.1" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.12.1" - "@babel/plugin-proposal-numeric-separator" "^7.12.7" - "@babel/plugin-proposal-object-rest-spread" "^7.12.1" - "@babel/plugin-proposal-optional-catch-binding" "^7.12.1" - "@babel/plugin-proposal-optional-chaining" "^7.12.7" - "@babel/plugin-proposal-private-methods" "^7.12.1" - "@babel/plugin-proposal-unicode-property-regex" "^7.12.1" - "@babel/plugin-syntax-async-generators" "^7.8.0" - "@babel/plugin-syntax-class-properties" "^7.12.1" - "@babel/plugin-syntax-dynamic-import" "^7.8.0" +"@babel/plugin-transform-shorthand-properties@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.0.tgz#090372e3141f7cc324ed70b3daf5379df2fa384d" + integrity sha512-iVb1mTcD8fuhSv3k99+5tlXu5N0v8/DPm2mO3WACLG6al1CGZH7v09HJyUb1TtYl/Z+KrM6pHSIJdZxP5A+xow== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-spread@^7.14.6": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.0.tgz#d21ca099bbd53ab307a8621e019a7bd0f40cdcfb" + integrity sha512-Ao4MSYRaLAQczZVp9/7E7QHsCuK92yHRrmVNRe/SlEJjhzivq0BSn8mEraimL8wizHZ3fuaHxKH0iwzI13GyGg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + +"@babel/plugin-transform-sticky-regex@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.0.tgz#c35ea31a02d86be485f6aa510184b677a91738fd" + integrity sha512-/ntT2NljR9foobKk4E/YyOSwcGUXtYWv5tinMK/3RkypyNBNdhHUaq6Orw5DWq9ZcNlS03BIlEALFeQgeVAo4Q== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-template-literals@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.0.tgz#a8eced3a8e7b8e2d40ec4ec4548a45912630d302" + integrity sha512-Rd4Ic89hA/f7xUSJQk5PnC+4so50vBoBfxjdQAdvngwidM8jYIBVxBZ/sARxD4e0yMXRbJVDrYf7dyRtIIKT6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-typeof-symbol@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.0.tgz#8b19a244c6f8c9d668dca6a6f754ad6ead1128f2" + integrity sha512-++V2L8Bdf4vcaHi2raILnptTBjGEFxn5315YU+e8+EqXIucA+q349qWngCLpUYqqv233suJ6NOienIVUpS9cqg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-unicode-escapes@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.0.tgz#1a354064b4c45663a32334f46fa0cf6100b5b1f3" + integrity sha512-VFi4dhgJM7Bpk8lRc5CMaRGlKZ29W9C3geZjt9beuzSUrlJxsNwX7ReLwaL6WEvsOf2EQkyIJEPtF8EXjB/g2A== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-unicode-regex@^7.14.5": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.0.tgz#293b80950177c8c85aede87cef280259fb995402" + integrity sha512-jHLK4LxhHjvCeZDWyA9c+P9XH1sOxRd1RO9xMtDVRAOND/PczPqizEtVdx4TQF/wyPaewqpT+tgQFYMnN/P94A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.0" + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/preset-env@7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.14.8.tgz#254942f5ca80ccabcfbb2a9f524c74bca574005b" + integrity sha512-a9aOppDU93oArQ51H+B8M1vH+tayZbuBqzjOhntGetZVa+4tTu5jp+XTwqHGG2lxslqomPYVSjIxQkFwXzgnxg== + dependencies: + "@babel/compat-data" "^7.14.7" + "@babel/helper-compilation-targets" "^7.14.5" + "@babel/helper-plugin-utils" "^7.14.5" + "@babel/helper-validator-option" "^7.14.5" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.14.5" + "@babel/plugin-proposal-async-generator-functions" "^7.14.7" + "@babel/plugin-proposal-class-properties" "^7.14.5" + "@babel/plugin-proposal-class-static-block" "^7.14.5" + "@babel/plugin-proposal-dynamic-import" "^7.14.5" + "@babel/plugin-proposal-export-namespace-from" "^7.14.5" + "@babel/plugin-proposal-json-strings" "^7.14.5" + "@babel/plugin-proposal-logical-assignment-operators" "^7.14.5" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.14.5" + "@babel/plugin-proposal-numeric-separator" "^7.14.5" + "@babel/plugin-proposal-object-rest-spread" "^7.14.7" + "@babel/plugin-proposal-optional-catch-binding" "^7.14.5" + "@babel/plugin-proposal-optional-chaining" "^7.14.5" + "@babel/plugin-proposal-private-methods" "^7.14.5" + "@babel/plugin-proposal-private-property-in-object" "^7.14.5" + "@babel/plugin-proposal-unicode-property-regex" "^7.14.5" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.0" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.0" - "@babel/plugin-syntax-top-level-await" "^7.12.1" - "@babel/plugin-transform-arrow-functions" "^7.12.1" - "@babel/plugin-transform-async-to-generator" "^7.12.1" - "@babel/plugin-transform-block-scoped-functions" "^7.12.1" - "@babel/plugin-transform-block-scoping" "^7.12.11" - "@babel/plugin-transform-classes" "^7.12.1" - "@babel/plugin-transform-computed-properties" "^7.12.1" - "@babel/plugin-transform-destructuring" "^7.12.1" - "@babel/plugin-transform-dotall-regex" "^7.12.1" - "@babel/plugin-transform-duplicate-keys" "^7.12.1" - "@babel/plugin-transform-exponentiation-operator" "^7.12.1" - "@babel/plugin-transform-for-of" "^7.12.1" - "@babel/plugin-transform-function-name" "^7.12.1" - "@babel/plugin-transform-literals" "^7.12.1" - "@babel/plugin-transform-member-expression-literals" "^7.12.1" - "@babel/plugin-transform-modules-amd" "^7.12.1" - "@babel/plugin-transform-modules-commonjs" "^7.12.1" - "@babel/plugin-transform-modules-systemjs" "^7.12.1" - "@babel/plugin-transform-modules-umd" "^7.12.1" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.12.1" - "@babel/plugin-transform-new-target" "^7.12.1" - "@babel/plugin-transform-object-super" "^7.12.1" - "@babel/plugin-transform-parameters" "^7.12.1" - "@babel/plugin-transform-property-literals" "^7.12.1" - "@babel/plugin-transform-regenerator" "^7.12.1" - "@babel/plugin-transform-reserved-words" "^7.12.1" - "@babel/plugin-transform-shorthand-properties" "^7.12.1" - "@babel/plugin-transform-spread" "^7.12.1" - "@babel/plugin-transform-sticky-regex" "^7.12.7" - "@babel/plugin-transform-template-literals" "^7.12.1" - "@babel/plugin-transform-typeof-symbol" "^7.12.10" - "@babel/plugin-transform-unicode-escapes" "^7.12.1" - "@babel/plugin-transform-unicode-regex" "^7.12.1" - "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.12.11" - core-js-compat "^3.8.0" - semver "^5.5.0" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.14.5" + "@babel/plugin-transform-async-to-generator" "^7.14.5" + "@babel/plugin-transform-block-scoped-functions" "^7.14.5" + "@babel/plugin-transform-block-scoping" "^7.14.5" + "@babel/plugin-transform-classes" "^7.14.5" + "@babel/plugin-transform-computed-properties" "^7.14.5" + "@babel/plugin-transform-destructuring" "^7.14.7" + "@babel/plugin-transform-dotall-regex" "^7.14.5" + "@babel/plugin-transform-duplicate-keys" "^7.14.5" + "@babel/plugin-transform-exponentiation-operator" "^7.14.5" + "@babel/plugin-transform-for-of" "^7.14.5" + "@babel/plugin-transform-function-name" "^7.14.5" + "@babel/plugin-transform-literals" "^7.14.5" + "@babel/plugin-transform-member-expression-literals" "^7.14.5" + "@babel/plugin-transform-modules-amd" "^7.14.5" + "@babel/plugin-transform-modules-commonjs" "^7.14.5" + "@babel/plugin-transform-modules-systemjs" "^7.14.5" + "@babel/plugin-transform-modules-umd" "^7.14.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.14.7" + "@babel/plugin-transform-new-target" "^7.14.5" + "@babel/plugin-transform-object-super" "^7.14.5" + "@babel/plugin-transform-parameters" "^7.14.5" + "@babel/plugin-transform-property-literals" "^7.14.5" + "@babel/plugin-transform-regenerator" "^7.14.5" + "@babel/plugin-transform-reserved-words" "^7.14.5" + "@babel/plugin-transform-shorthand-properties" "^7.14.5" + "@babel/plugin-transform-spread" "^7.14.6" + "@babel/plugin-transform-sticky-regex" "^7.14.5" + "@babel/plugin-transform-template-literals" "^7.14.5" + "@babel/plugin-transform-typeof-symbol" "^7.14.5" + "@babel/plugin-transform-unicode-escapes" "^7.14.5" + "@babel/plugin-transform-unicode-regex" "^7.14.5" + "@babel/preset-modules" "^0.1.4" + "@babel/types" "^7.14.8" + babel-plugin-polyfill-corejs2 "^0.2.2" + babel-plugin-polyfill-corejs3 "^0.2.2" + babel-plugin-polyfill-regenerator "^0.2.2" + core-js-compat "^3.15.0" + semver "^6.3.0" -"@babel/preset-modules@^0.1.3": - version "0.1.4" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.4.tgz#362f2b68c662842970fdb5e254ffc8fc1c2e415e" - integrity sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg== +"@babel/preset-modules@^0.1.4": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" @@ -1295,114 +1191,99 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/runtime@7.12.5", "@babel/runtime@^7.12.5": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== +"@babel/runtime@7.14.8": + version "7.14.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" + integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== +"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@7.12.7": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" - integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.12.7" - "@babel/types" "^7.12.7" - -"@babel/template@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/template@^7.12.13", "@babel/template@^7.12.7": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" - integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/parser" "^7.12.13" - "@babel/types" "^7.12.13" - -"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.5": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" - integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.5" - "@babel/types" "^7.11.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/traverse@^7.12.10", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.15", "@babel/traverse@^7.14.0", "@babel/traverse@^7.14.2": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.2.tgz#9201a8d912723a831c2679c7ebbf2fe1416d765b" - integrity sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA== - dependencies: - "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.14.2" - "@babel/helper-function-name" "^7.14.2" - "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.14.2" - "@babel/types" "^7.14.2" +"@babel/template@7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.14.5.tgz#a9bc9d8b33354ff6e55a9c60d1109200a68974f4" + integrity sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g== + dependencies: + "@babel/code-frame" "^7.14.5" + "@babel/parser" "^7.14.5" + "@babel/types" "^7.14.5" + +"@babel/template@^7.14.5", "@babel/template@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6" + integrity sha512-MnZdpFD/ZdYhXwiunMqqgyZyucaYsbL0IrjoGjaVhGilz+x8YB++kRfygSOIj1yOtWKPlx7NBp+9I1RQSgsd5A== + dependencies: + "@babel/code-frame" "^7.16.0" + "@babel/parser" "^7.16.0" + "@babel/types" "^7.16.0" + +"@babel/traverse@^7.13.0", "@babel/traverse@^7.14.8", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.3": + version "7.16.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787" + integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag== + dependencies: + "@babel/code-frame" "^7.16.0" + "@babel/generator" "^7.16.0" + "@babel/helper-function-name" "^7.16.0" + "@babel/helper-hoist-variables" "^7.16.0" + "@babel/helper-split-export-declaration" "^7.16.0" + "@babel/parser" "^7.16.3" + "@babel/types" "^7.16.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.4.4": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" - integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== +"@babel/types@^7.14.5", "@babel/types@^7.14.8", "@babel/types@^7.16.0", "@babel/types@^7.4.4", "@babel/types@^7.8.6": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.16.0.tgz#db3b313804f96aadd0b776c4823e127ad67289ba" + integrity sha512-PJgg/k3SdLsGb3hhisFvtLOw5ts113klrpLuIPtCJIU+BB24fqq6lf8RWqKJEjzqXR9AEH1rIb5XTqwBHB+kQg== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" + "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" -"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.16", "@babel/types@^7.14.0", "@babel/types@^7.14.2", "@babel/types@^7.8.6": - version "7.14.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.2.tgz#4208ae003107ef8a057ea8333e56eb64d2f6a2c3" - integrity sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw== +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== dependencies: - "@babel/helper-validator-identifier" "^7.14.0" - to-fast-properties "^2.0.0" + "@cspotcode/source-map-consumer" "0.8.0" + +"@csstools/convert-colors@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7" + integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw== "@date-io/core@1.x": version "1.3.13" resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== -"@date-io/core@^2.10.11": - version "2.10.11" - resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.10.11.tgz#b1a3d57730f3eaaab54d5658be4a71727297e357" - integrity sha512-keXQnwH0LM8wyvu+j5Z2KGK56D+eItjy7DnwuWl/oV+DM2UEYl0z5WhdPMpfswSyt/kjuPOzcVF/7u/skMLaoA== +"@date-io/core@^2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.11.0.tgz#28580cda1c8228ab2c7ed6aee673ef0495f913e6" + integrity sha512-DvPBnNoeuLaoSJZaxgpu54qzRhRKjSYVyQjhznTFrllKuDpm0sDFjHo6lvNLCM/cfMx2gb2PM2zY2kc9C8nmuw== -"@date-io/date-fns@^2.10.11": - version "2.10.11" - resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.10.11.tgz#1f3b40c6f2ea2e659260ca329fef80cb6dc64704" - integrity sha512-QG9IAZ4bvwkJftoSVKtdb5ISH+Qp4zilrjhzcL4RXaeqkfIWiFeXqQPgJljvPl6gQ04zf2SjGkWjdh1eJxxwmQ== +"@date-io/date-fns@^2.11.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.11.0.tgz#142fbf954eda7ad66514af7a2802d78c4ea40053" + integrity sha512-mPQ71plBeFrArvBSHtjWMHXA89IUbZ6kuo2dsjlRC/1uNOybo91spIb+wTu03NxKTl8ut07s0jJ9svF71afpRg== dependencies: - "@date-io/core" "^2.10.11" + "@date-io/core" "^2.11.0" -"@discoveryjs/json-ext@0.5.2": - version "0.5.2" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752" - integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg== +"@discoveryjs/json-ext@0.5.3": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d" + integrity sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g== "@emotion/hash@^0.8.0": version "0.8.0" @@ -1422,10 +1303,37 @@ "@types/flowjs" "2.13.3" tslib "^1.9.0" +"@gar/promisify@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" + integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== + +"@geoman-io/leaflet-geoman-free@^2.11.3": + version "2.11.3" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.11.3.tgz#480164ab76c2b2a885003e0c111284f3c3160a36" + integrity sha512-LsiurEgKEHBcTnAVl8h7EfS5V/doCuxePzPE9SnfrhtJBN7IzP6UwkEo35Agwko+BnIuw/o2bE4F7irvKwQzjw== + dependencies: + "@turf/boolean-contains" "6.3.0" + "@turf/kinks" "6.3.0" + "@turf/line-intersect" "6.3.0" + "@turf/line-split" "6.3.0" + lodash "4.17.21" + polygon-clipping "0.15.3" + "@istanbuljs/schema@^0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" - integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jcubic/lily@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@jcubic/lily/-/lily-0.3.0.tgz#00b229aab69fe094a57fd37f27d32dd4f380c1d1" + integrity sha512-4z6p4jLGSthc8gQ7wu4nHfGYn/IgCKFr+7hjuf80VdXUs7sm029mZGGDpS8sb29PVZWUBvMMTBCVGFhH2nN4Vw== + +"@jridgewell/resolve-uri@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-1.0.0.tgz#3fdf5798f0b49e90155896f6291df186eac06c83" + integrity sha512-9oLAnygRMi8Q5QkYEU4XWK04B+nuoXoxjRvRxgjuChkLZFBja0YPSgdZ7dZtwhncLBcQe/I/E+fLuk5qxcYVJA== "@jsdevtools/coverage-istanbul-loader@3.0.5": version "3.0.5" @@ -1443,21 +1351,21 @@ resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0" integrity sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw== -"@mat-datetimepicker/core@~6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@mat-datetimepicker/core/-/core-6.0.2.tgz#c4f00fee4780e116ad0b685dcdf1014ad81301c5" - integrity sha512-cBzbg9GNOcV3eyhXa/44TTlBIy0t4Qgp1agSrhUoMucnFzhlIcF0LrUswOOQaldXDw1FQUQE5VQz4KSFjac8+g== +"@mat-datetimepicker/core@~7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@mat-datetimepicker/core/-/core-7.0.1.tgz#dac547195f25d448cfeaa11e0384f59639065c5f" + integrity sha512-lTYFJYstVb5l5JuNwVVZeyMaDtkZIq+eKycUa+5aJBAPhjapwdJx6lHiaZODgydRNtzdw79pQcB00mufguv3ew== dependencies: - tslib "^2.0.0" + tslib "^2.3.0" -"@material-ui/core@^4.11.4": - version "4.11.4" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.4.tgz#4fb9fe5dec5dcf780b687e3a40cff78b2b9640a4" - integrity sha512-oqb+lJ2Dl9HXI9orc6/aN8ZIAMkeThufA5iZELf2LQeBn2NtjVilF5D2w7e9RpntAzDb4jK5DsVhkfOvFY/8fg== +"@material-ui/core@^4.12.3": + version "4.12.3" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" + integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/styles" "^4.11.4" - "@material-ui/system" "^4.11.3" + "@material-ui/system" "^4.12.1" "@material-ui/types" "5.1.0" "@material-ui/utils" "^4.11.2" "@types/react-transition-group" "^4.2.0" @@ -1509,10 +1417,10 @@ jss-plugin-vendor-prefixer "^10.5.1" prop-types "^15.7.2" -"@material-ui/system@^4.11.3": - version "4.11.3" - resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.11.3.tgz#466bc14c9986798fd325665927c963eb47cc4143" - integrity sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw== +"@material-ui/system@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c" + integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/utils" "^4.11.2" @@ -1533,35 +1441,31 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@ngrx/effects@^10.1.2": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-10.1.2.tgz#f1c9daa2e3e13aeb6af272cb52f6dff73d364d68" - integrity sha512-6pX6FEzLlqdbtFVMbCvscsaL6QC/L95e72JKj76Xz+8V77UTlpVsxWyMo7YU9pM4EXNpBGmOpMs2xKjfBfK05Q== +"@ngrx/effects@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-12.5.1.tgz#acd0ff86d8db514e47337508dde83cc98f7a3416" + integrity sha512-fVNGIIntYLRWW1XWe0os2XOv03L22S4WTkX0OPZ9O6ztwuaNq0yzxWN7UeAC6H385F+g0k76KwRV78zHyP0bfQ== dependencies: tslib "^2.0.0" -"@ngrx/store-devtools@^10.1.2": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-10.1.2.tgz#1dba8c84df4b56a2b15d6abc01c3c378d1518830" - integrity sha512-HE681GuZ+lRgSXpgt7y7LKzsfu/+Tgy9yPZpaitvkhg+eCIjnN5Uvs1rWqETHYWnsKliW25yoqFUAVw+xb7hug== +"@ngrx/store-devtools@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-12.5.1.tgz#75f8ef9a4bf4a40d5343ff437651f0f3092914b5" + integrity sha512-SXMxVO3KzQUfB9G20gdNT5t/RcbtbaUySXLuH+b69z/eb34wH9AOYifdSdcEi8oqPjDrWYBq6a8Uh+yDHf9IfA== dependencies: tslib "^2.0.0" -"@ngrx/store@^10.1.2": - version "10.1.2" - resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-10.1.2.tgz#a41aee81ea7b7e4a9f927be560e6024830211efb" - integrity sha512-FUjN786ch4Qt9WgJ78ef7Yquq3mPCekgcWgZrs4ycZw1f+KdfTHLTk1bGDtO8A8CzOya5yTT7KhxbdVjbOS5ng== +"@ngrx/store@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-12.5.1.tgz#a7c21d7df1d017d2cb7e77804b210cc14bcf8786" + integrity sha512-NLVkHLVeZc7IboXSDZlFoq1QrupmwYTYKRHS6se7ZasAv/lrIjHWsVVdICKSVRBsHZYu3+dmCXmu+YgulP7iHw== dependencies: tslib "^2.0.0" -"@ngtools/webpack@11.2.13", "@ngtools/webpack@~11.2.13": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.13.tgz#dbd77deb37ab57fd98678bd637fe87693fd71f4f" - integrity sha512-Wbv1zjLaS1zXcJ1wryYc6vhKWq8r/+ZrdN5SlRIf3xf7fwuTymbvL41SndUibssXir6s4vePYqZL241nkThx9g== - dependencies: - "@angular-devkit/core" "11.2.13" - enhanced-resolve "5.7.0" - webpack-sources "2.2.0" +"@ngtools/webpack@12.2.13", "@ngtools/webpack@~12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-12.2.13.tgz#44d711edfa39d175a2d655aa5c6f9968dfedcc35" + integrity sha512-krAwMyRqOaC1S0+IxAFid9F6A6ABip2DJ0tgCx26X+1Vw/d1GKtV9ZqDJFizMf5k1ywl9aBlhOazWpq5d6i+gw== "@ngx-translate/core@^13.0.0": version "13.0.0" @@ -1577,36 +1481,39 @@ dependencies: tslib "^2.0.0" -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== dependencies: - "@nodelib/fs.stat" "2.0.3" + "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== "@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: - "@nodelib/fs.scandir" "2.1.3" + "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@npmcli/ci-detect@^1.0.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" - integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== +"@npmcli/fs@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.0.0.tgz#589612cfad3a6ea0feafcb901d29c63fd52db09f" + integrity sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" -"@npmcli/git@^2.0.1": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.0.9.tgz#915bbfe66300e67b4da5ef765a4475ffb2ca5b6b" - integrity sha512-hTMbMryvOqGLwnmMBKs5usbPsJtyEsMsgXwJbmNrsEuQQh1LAIMDU77IoOrwkCg+NgQWl+ySlarJASwM3SutCA== +"@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.1.0.tgz#2fbd77e147530247d37f325930d457b3ebe894f6" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== dependencies: "@npmcli/promise-spawn" "^1.3.2" lru-cache "^6.0.0" @@ -1617,7 +1524,7 @@ semver "^7.3.5" which "^2.0.2" -"@npmcli/installed-package-contents@^1.0.5": +"@npmcli/installed-package-contents@^1.0.6": version "1.0.7" resolved "https://registry.yarnpkg.com/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== @@ -1626,16 +1533,17 @@ npm-normalize-package-bin "^1.0.1" "@npmcli/move-file@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" - integrity sha512-Uv6h1sT+0DrblvIrolFtbvM1FgWm+/sy4B3pvLp67Zys+thcukzS5ekn7HsZFGpWP4Q3fYJCljbWQE/XivMRLw== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== dependencies: mkdirp "^1.0.4" + rimraf "^3.0.2" "@npmcli/node-gyp@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz#3cdc1f30e9736dbc417373ed803b42b1a0a29ede" - integrity sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz#a912e637418ffc5f2db375e93b85837691a43a33" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": version "1.3.2" @@ -1644,74 +1552,261 @@ dependencies: infer-owner "^1.0.4" -"@npmcli/run-script@^1.3.0": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.5.tgz#f250a0c5e1a08a792d775a315d0ff42fc3a51e1d" - integrity sha512-NQspusBCpTjNwNRFMtz2C5MxoxyzlbuJ4YEhxAKrIonTiirKDtatsZictx9RgamQIx6+QuHMNmPl0wQdoESs9A== +"@npmcli/run-script@^1.8.2": + version "1.8.6" + resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.6.tgz#18314802a6660b0d4baa4c3afe7f1ad39d8c28b7" + integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== dependencies: "@npmcli/node-gyp" "^1.0.2" "@npmcli/promise-spawn" "^1.3.2" - infer-owner "^1.0.4" node-gyp "^7.1.0" read-package-json-fast "^2.0.1" -"@schematics/angular@11.2.13": - version "11.2.13" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.13.tgz#a37e9e7a2cf0a37a8948f0afb0f29e6671ce740a" - integrity sha512-cMXWruwSkH2XEKDA8df70Ey/5okdJrRCgsnXkGuLRC+GQM7ArLPkgoVw0GXM8TfI0bWyGWT7QYbHBzsyN5ajGA== +"@schematics/angular@12.2.13": + version "12.2.13" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-12.2.13.tgz#5bf3e7b699a42d7fd7f7aa12bbe4534e671e7201" + integrity sha512-TrigQ9TCmAedf1J5PSSSfTC+sScYrITeAUN8a9rlkjZNvff8hHVyQaiZmhqL+egKQL828mhkqpnFUDd4QsPBIw== dependencies: - "@angular-devkit/core" "11.2.13" - "@angular-devkit/schematics" "11.2.13" + "@angular-devkit/core" "12.2.13" + "@angular-devkit/schematics" "12.2.13" jsonc-parser "3.0.0" -"@schematics/update@0.1102.13": - version "0.1102.13" - resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.13.tgz#d6e5a3df5bf22900f26dc76a8351651f99a655fb" - integrity sha512-lHym4eUhbvUORwRJQJzgVFjofnc9nB2f/Ef3NNEgn3OkvsxHWtPH8NCQy/tj6sBAsyqK2T88aO2nejU9fZgMog== - dependencies: - "@angular-devkit/core" "11.2.13" - "@angular-devkit/schematics" "11.2.13" - "@yarnpkg/lockfile" "1.1.0" - ini "2.0.0" - npm-package-arg "^8.0.0" - pacote "11.2.4" - semver "7.3.4" - semver-intersect "1.4.0" - "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@trysound/sax@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669" - integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow== +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/canvas-gauges@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/canvas-gauges/-/canvas-gauges-2.1.2.tgz#fb9ece324cb15ae137791ad21eb2db70e11a7210" - integrity sha512-oWCq0XjsTBXPtMKXoW23ORbMWguC2Fa8o5NiZVYiUoQMMrpNLKj1E+LDznlMpcib3iyWVIy+TEpc/ea6LMbW3Q== +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + +"@turf/bbox@*", "@turf/bbox@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.5.0.tgz#bec30a744019eae420dac9ea46fb75caa44d8dc5" + integrity sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@turf/bearing@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/bearing/-/bearing-6.5.0.tgz#462a053c6c644434bdb636b39f8f43fb0cd857b0" + integrity sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/boolean-contains@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.3.0.tgz#fe4fc359e408c8c3c89e7fb159c9d31fde48779a" + integrity sha512-1MW7B5G5tIu1lnAv3pXyFzl75wfBYnbA2GhwHDb4okIXMhloy/r5uIqAZHo0fOXykKVJS/gIfA/MioKIftoTug== + dependencies: + "@turf/bbox" "^6.3.0" + "@turf/boolean-point-in-polygon" "^6.3.0" + "@turf/boolean-point-on-line" "^6.3.0" + "@turf/helpers" "^6.3.0" + "@turf/invariant" "^6.3.0" + +"@turf/boolean-point-in-polygon@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz#6d2e9c89de4cd2e4365004c1e51490b7795a63cf" + integrity sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/boolean-point-on-line@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz#a8efa7bad88760676f395afb9980746bc5b376e9" + integrity sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/destination@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/destination/-/destination-6.5.0.tgz#30a84702f9677d076130e0440d3223ae503fdae1" + integrity sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/distance@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/distance/-/distance-6.5.0.tgz#21f04d5f86e864d54e2abde16f35c15b4f36149a" + integrity sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + +"@turf/helpers@6.x", "@turf/helpers@^6.3.0", "@turf/helpers@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" + integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== + +"@turf/invariant@^6.3.0", "@turf/invariant@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" + integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== + dependencies: + "@turf/helpers" "^6.5.0" + +"@turf/kinks@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.3.0.tgz#a16b4ccc5a5aae139d43e36271e0a0494fdb4bf7" + integrity sha512-BLWvbl2/fa4SeJzVMbleT6Vo1cmzwmzRfxL2xxMei2jmf6JSvqDoMJFwIHGXrLZXvhOCb1b2C+MhBfhtc7kYkQ== + dependencies: + "@turf/helpers" "^6.3.0" + +"@turf/line-intersect@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.3.0.tgz#726a50edc66bb7b5e798b052b103fb0da4d1c4f4" + integrity sha512-3naxR7XpkPd2vst3Mw6DFry4C9m3o0/f2n/xu5UAyxb88Ie4m2k+1eqkhzMMx/0L+E6iThWpLx7DASM6q6o9ow== + dependencies: + "@turf/helpers" "^6.3.0" + "@turf/invariant" "^6.3.0" + "@turf/line-segment" "^6.3.0" + "@turf/meta" "^6.3.0" + geojson-rbush "3.x" + +"@turf/line-intersect@^6.3.0", "@turf/line-intersect@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.5.0.tgz#dea48348b30c093715d2195d2dd7524aee4cf020" + integrity sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-segment" "^6.5.0" + "@turf/meta" "^6.5.0" + geojson-rbush "3.x" + +"@turf/line-segment@^6.3.0", "@turf/line-segment@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-segment/-/line-segment-6.5.0.tgz#ee73f3ffcb7c956203b64ed966d96af380a4dd65" + integrity sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@turf/line-split@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.3.0.tgz#ee218f66cd65ce84eafc4956c24083663f6082ea" + integrity sha512-Q0nUJ0vczy11piyEz0FaKScFwSQtb1HJ2RPEMCw1coUJhTCB02KBWQLImhYqwsD3uLg+H/fxaJ1Gva6EPWoDNQ== + dependencies: + "@turf/bbox" "^6.3.0" + "@turf/helpers" "^6.3.0" + "@turf/invariant" "^6.3.0" + "@turf/line-intersect" "^6.3.0" + "@turf/line-segment" "^6.3.0" + "@turf/meta" "^6.3.0" + "@turf/nearest-point-on-line" "^6.3.0" + "@turf/square" "^6.3.0" + "@turf/truncate" "^6.3.0" + geojson-rbush "3.x" + +"@turf/meta@6.x", "@turf/meta@^6.3.0", "@turf/meta@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.5.0.tgz#b725c3653c9f432133eaa04d3421f7e51e0418ca" + integrity sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA== + dependencies: + "@turf/helpers" "^6.5.0" + +"@turf/nearest-point-on-line@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz#8e1cd2cdc0b5acaf4c8d8b3b33bb008d3cb99e7b" + integrity sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg== + dependencies: + "@turf/bearing" "^6.5.0" + "@turf/destination" "^6.5.0" + "@turf/distance" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@turf/square@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/square/-/square-6.5.0.tgz#ab43eef99d39c36157ab5b80416bbeba1f6b2122" + integrity sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ== + dependencies: + "@turf/distance" "^6.5.0" + "@turf/helpers" "^6.5.0" + +"@turf/truncate@^6.3.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/truncate/-/truncate-6.5.0.tgz#c3a16cad959f1be1c5156157d5555c64b19185d8" + integrity sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg== + dependencies: + "@turf/helpers" "^6.5.0" + "@turf/meta" "^6.5.0" + +"@types/canvas-gauges@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/canvas-gauges/-/canvas-gauges-2.1.4.tgz#063881264597d098e78cf5ad921e8ed20ae2ad16" + integrity sha512-JTvqQWrqcrgzCp/9+uzwUvPef2qAEnBJvm+bL9kvulzhXapDeNaGQXCIAZp+hOryusjOyndvP1za2HZooUV0XA== "@types/component-emitter@^1.2.10": - version "1.2.10" - resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" - integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== + version "1.2.11" + resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506" + integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ== -"@types/cookie@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" - integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== -"@types/cors@^2.8.8": - version "2.8.10" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" - integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== +"@types/cors@^2.8.12": + version "2.8.12" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== -"@types/flot@^0.0.31": - version "0.0.31" - resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.31.tgz#0daca37c6c855b69a0a7e2e37dd0f84b3db8c8c1" - integrity sha512-X+RcMQCqPlQo8zPT6cUFTd/PoYBShMQlHUeOXf05jWlfYnvLuRmluB9z+2EsOKFgUzqzZve5brx+gnFxBaHEUw== +"@types/eslint-scope@^3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e" + integrity sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.2.0.tgz#afd0519223c29c347087542cbaee2fedc0873b16" + integrity sha512-74hbvsnc+7TEDa1z5YLSe4/q8hGYB3USNvCuzHUJrjPV6hXaq8IXcngCrHkuvFt0+8rFz7xYXrHgNayIX0UZvQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^0.0.50": + version "0.0.50" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + +"@types/flot@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.32.tgz#2ab260f2958dcab1acfb5c24b87898f1d22417d8" + integrity sha512-aturel4TWMY86N4Pkpc9pSoUd/p8c3BjGj4fTDkaZIpkRPzLH1VXZCAKGUywcFkTqgZMhPJFPWxd4pl87y8h/w== dependencies: "@types/jquery" "*" @@ -1720,109 +1815,104 @@ resolved "https://registry.yarnpkg.com/@types/flowjs/-/flowjs-2.13.3.tgz#4f1ba77d9259f4be83ecaa985db96fa758b2fd22" integrity sha512-VeWuL+Whk6lUSWX/g0LzLNyZywyTB5wZ2L6mPvD8/u5pgLF2HwyV7nZ1UArOifalJ5UE1CcJbPLKS+jc5+Z2ig== -"@types/geojson@*": - version "7946.0.7" - resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" - integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== +"@types/geojson@*", "@types/geojson@7946.0.8": + version "7946.0.8" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" + integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== "@types/glob@^7.1.1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" - integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== dependencies: "@types/minimatch" "*" "@types/node" "*" -"@types/jasmine@*": - version "3.5.14" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.5.14.tgz#f41a14e8ffa939062a71cf9722e5ee7d4e1f94af" - integrity sha512-Fkgk536sHPqcOtd+Ow+WiUNuk0TSo/BntKkF8wSvcd6M2FvPjeXcUE6Oz/bwDZiUZEaXLslAgw00Q94Pnx6T4w== +"@types/hammerjs@^2.0.39": + version "2.0.40" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.40.tgz#ded0240b6ea1ad7afc1e60374c49087aaea5dbd8" + integrity sha512-VbjwR1fhsn2h2KXAY4oy1fm7dCxaKy0D+deTb8Ilc3Eo3rc5+5eA4rfYmZaHgNJKxVyI0f6WIXzO2zLkVmQPHA== -"@types/jasmine@~3.7.4": - version "3.7.4" - resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.7.4.tgz#99a49aa9a5f8dc86fc249ed13ed59552c6ce862d" - integrity sha512-L3FKeEwMm8e8hqGvt7cSesVmGtavpRyHV1FNDq+Pm5pS4x5eFmE70ZERXCSBWAiLQqXBcZRUrwV59FZLQl/GxQ== +"@types/jasmine@*", "@types/jasmine@~3.10.2": + version "3.10.2" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.10.2.tgz#1b9f9ba9ad7bfd7d322f7ed9d8753220b1c84b52" + integrity sha512-qs4xjVm4V/XjM6owGm/x6TNmhGl5iKX8dkTdsgdgl9oFnqgzxLepnS7rN9Tdo7kDmnFD/VEqKrW57cGD2odbEg== -"@types/jasminewd2@^2.0.9": - version "2.0.9" - resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.9.tgz#db3946314605deea9d5c7aea0b61d807089ba76d" - integrity sha512-Oz+Faunpe2SimFvkMYMXxpK89WXl7rZHG8abTOKcGndu4xOoSbUZ+jUdZ0LQpmDqPEGLBWXF/yZP1tlsplGhzw== +"@types/jasminewd2@^2.0.10": + version "2.0.10" + resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.10.tgz#ae31c237aa6421bde30f1058b1d20f4577e54443" + integrity sha512-J7mDz7ovjwjc+Y9rR9rY53hFWKATcIkrr9DwQWmOas4/pnIPJTXawnzjwpHm3RSxz/e3ZVUvQ7cRbd5UQLo10g== dependencies: "@types/jasmine" "*" -"@types/jquery@*", "@types/jquery@^3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.2.tgz#e17c1756ecf7bbb431766c6761674a5d1de16579" - integrity sha512-+MFOdKF5Zr41t3y2wfzJvK1PrUK0KtPLAFwYownp/0nCoMIANDDu5aFSpWfb8S0ZajCSNeaBnMrBGxksXK5yeg== - dependencies: - "@types/sizzle" "*" - -"@types/jquery@^3.5.5": - version "3.5.5" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.5.tgz#2c63f47c9c8d96693d272f5453602afd8338c903" - integrity sha512-6RXU9Xzpc6vxNrS6FPPapN1SxSHgQ336WC6Jj/N8q30OiaBZ00l1GBgeP7usjVZPivSkGUfL1z/WW6TX989M+w== +"@types/jquery@*", "@types/jquery@^3.5.6", "@types/jquery@^3.5.9": + version "3.5.9" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.9.tgz#08e4b1dcff0626926ffe545a2bc90f18140c53d6" + integrity sha512-B8pDk+sH/tSv/HKdx6EQER6BfUOb2GtKs0LOmozziS4h7cbe8u/eYySfUAeTwD+J09SqV3man7AMWIA5mgzCBA== dependencies: "@types/sizzle" "*" -"@types/js-beautify@^1.13.1": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.13.1.tgz#d4739266c5dcad561226cd1ec5407fa0542d863d" - integrity sha512-F3YCoZS//n74Wu+hxoVrxX1H8qaWo+WAgQ+ObmFH4ZFwI0fIwiJTW7pvkCRShw8ST7+ej7sB68K+ZHAZgK4S4Q== - -"@types/json-schema@^7.0.5": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" - integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== +"@types/js-beautify@^1.13.3": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.13.3.tgz#53839bb5b766d0fb45e87386100bb3bcbb7dca9d" + integrity sha512-ucIPw5gmNyvRKi6mpeojlqp+T+6ZBJeU+kqMDnIEDlijEU4QhLTon90sZ3cz9HZr+QTwXILjNsMZImzA7+zuJA== -"@types/json-schema@^7.0.6": - version "7.0.7" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" - integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/jstree@^3.3.40": - version "3.3.40" - resolved "https://registry.yarnpkg.com/@types/jstree/-/jstree-3.3.40.tgz#835737a262ea2572df9ffafc68c08633f4554aa4" - integrity sha512-+6mdAX+vaj962NSd1nnzSLBWD2obUQf5+1yxR+4/g+abpEIQFsI+CcqP4l+cS6H9P07sTONMbR3Z09bPpDzkNg== +"@types/jstree@^3.3.41": + version "3.3.41" + resolved "https://registry.yarnpkg.com/@types/jstree/-/jstree-3.3.41.tgz#820ce1f82bbb2441eaf9fb76451750e2810f5856" + integrity sha512-M4ia6tSKOAU/8v2Ir1Kyo3/XE4EWxw5ulnzbE/nmvw7YugxWhRfTIc6i2ubvJ3GQpdXzJ11bH+GT3VhJNcJadQ== dependencies: "@types/jquery" "*" -"@types/leaflet-editable@^1.2.1": +"@types/leaflet-polylinedecorator@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.1.tgz#b6522f9dae52146bf73da249e4bedfbab200c6e4" + integrity sha512-9etweJ2U4SWqcV/AR3i0NdWJByeMn6+zMUNlO6jVbpL8UI6qrMKybu8v9/s6UR4oXvsV4lZT6vzAsNAAMq5Ssg== + dependencies: + "@types/leaflet" "*" + +"@types/leaflet-providers@^1.2.1": version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/leaflet-editable/-/leaflet-editable-1.2.1.tgz#12f1bd1d9af7beafbac256216062e97fe2ee4d55" - integrity sha512-7Oms1HgulWiclkI0s1XLmr1yRylNoJX8sVUfAv9+28JzwWbKbLcQ6//vhFEOmoMlBQyL5veogKpUUb5qeF+Qyg== + resolved "https://registry.yarnpkg.com/@types/leaflet-providers/-/leaflet-providers-1.2.1.tgz#620669b828959740a2d8572e0c0288a2382d3564" + integrity sha512-uNyuXiNV2q3fmgNjQji2P6RjQISmL40bbOL91/3OAwiE3XhkLKPmSAtAcfe11MAIz45iEjdFZJWppq9QyfnPIw== dependencies: "@types/leaflet" "*" -"@types/leaflet-polylinedecorator@^1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@types/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz#1572131ffedb3154c6e18e682d2fb700e203af19" - integrity sha512-Z2BXZDjKEqHclwrAmhYdF1RwyFfa/NFxsoF79sitzaj5D/4YWHp/zDRcUZar5cQFKRgK66AYEIF7nKVuMzUGdw== +"@types/leaflet.gridlayer.googlemutant@^0.4.6": + version "0.4.6" + resolved "https://registry.yarnpkg.com/@types/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.4.6.tgz#86d3ba9d432dec29b4796e37d815c233680e7fcb" + integrity sha512-L0J7NadcZp5bcKQrv4DVlsEbQ90xLsOKScckAMnxoghh/wogk0GVkauYOYHBKeKDkx9qkMRzTf8oO+fKeYD7oQ== dependencies: "@types/leaflet" "*" -"@types/leaflet.markercluster@^1.4.4": - version "1.4.4" - resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.4.tgz#4b4772c86182923e920061a0c25cb3e53543ad35" - integrity sha512-BQAilNWlBpYl4+PrsJXLOh4vyv7KfWi5kh3Fclg5y4gEeNeXKqhS6y1zzBB4+wcTuVUnMWfm2G0MfqA4yA5A5A== +"@types/leaflet.markercluster@^1.4.6": + version "1.4.6" + resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.6.tgz#1159460b374ba5e329cb678d0e427f99dca75be5" + integrity sha512-MD+bUDzxHznY0zOlSBUAMNQUGB2+xpJPKrR2MNEoBAAKa3QTKJJySBtCqWyGLvYNNO+Cdyc2c64aF2IFwe4fcQ== dependencies: "@types/leaflet" "*" -"@types/leaflet@*", "@types/leaflet@1.5.17": - version "1.5.17" - resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.5.17.tgz#b2153dc12c344e6896a93ffc6b61ac79da251e5b" - integrity sha512-2XYq9k6kNjhNI7PaTz8Rdxcc8Vzwu97OaS9CtcrTxnTSxFUGwjlGjTDvhTLJU+JRSfZ4lBwGcl0SjZHALdVr6g== +"@types/leaflet@*", "@types/leaflet@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.6.tgz#6580f4babb648972c5af3abc3d66866753fa9311" + integrity sha512-Emkz3V08QnlelSbpT46OEAx+TBZYTOX2r1yM7W+hWg5+djHtQ1GbEXBDRLaqQDOYcDI51Ss0ayoqoKD4CtLUDA== dependencies: "@types/geojson" "*" -"@types/lodash@^4.14.170": - version "4.14.170" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.170.tgz#0d67711d4bf7f4ca5147e9091b847479b87925d6" - integrity sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q== +"@types/lodash@^4.14.177": + version "4.14.177" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.177.tgz#f70c0d19c30fab101cad46b52be60363c43c4578" + integrity sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw== "@types/marked@^2.0.0": version "2.0.5" @@ -1830,9 +1920,9 @@ integrity sha512-shRZ7XnYFD/8n8zSjKvFdto1QNSf4tONZIlNEZGrJe8GsOE8DL/hG1Hbl8gZlfLnjS7+f5tZGIaTgfpyW38h4w== "@types/minimatch@*": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" - integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== "@types/moment-timezone@^0.5.30": version "0.5.30" @@ -1841,25 +1931,20 @@ dependencies: moment-timezone "*" -"@types/mousetrap@1.6.3": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" - integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== - "@types/mousetrap@^1.6.0": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.4.tgz#32503197fca4168b10bf251c1d677a9b5b1c2415" - integrity sha512-+Y900DGhe+f+4lRwHm9krsKfsiXcbdOhzTsLbytU4MiG8wE9xOw7CFKtgYKfqEAcUdWEGZRyuTxoyFl2Gx6Rdg== + version "1.6.8" + resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.8.tgz#448929e6dc21126392830465fdb9d4a2cfc16a88" + integrity sha512-zTqjvgCUT5EoXqbqmd8iJMb4NJqyV/V7pK7AIKq7qcaAsJIpGlTVJS1HQM6YkdHCdnkNSbhcQI7MXYxFfE3iCA== -"@types/node@*": - version "14.11.5" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.11.5.tgz#fecad41c041cae7f2404ad4b2d0742fdb628b305" - integrity sha512-jVFzDV6NTbrLMxm4xDSIW/gKnk8rQLF9wAzLWIOg+5nU6ACrIMndeBdXci0FGtqJbP9tQvm6V39eshc96TO2wQ== +"@types/node@*", "@types/node@>=10.0.0": + version "16.11.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.11.tgz#6ea7342dfb379ea1210835bada87b3c512120234" + integrity sha512-KB0sixD67CeecHC33MYn+eYARkqTheIRNuu97y2XMjR7Wu3XibO1vaY6VBV6O/a89SPI81cEUIYT87UqUWlZNw== -"@types/node@>=10.0.0": - version "15.6.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.1.tgz#32d43390d5c62c5b6ec486a9bc9c59544de39a08" - integrity sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA== +"@types/node@~15.14.9": + version "15.14.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" + integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1867,51 +1952,57 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/prop-types@*": - version "15.7.3" - resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" - integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== "@types/q@^0.0.32": version "0.0.32" resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" integrity sha1-vShOV8hPEyXacCur/IKlMoGQwMU= -"@types/raphael@^2.3.1": - version "2.3.1" - resolved "https://registry.yarnpkg.com/@types/raphael/-/raphael-2.3.1.tgz#5e8e69c35f2c8a89a6e5cf0f9ff87004bbc6f3bb" - integrity sha512-zsocvfGMgQT9kVwGYthhWifd5iUw3nhzXZyO7+V9+B69kNYi6x0FN9W2k9dLJecXsk4HR6aKVmaSQjkWGo3N9w== +"@types/raphael@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@types/raphael/-/raphael-2.3.2.tgz#833eff033c33fb1a3935882bc29c82908cb9a734" + integrity sha512-RbEkYHuQbb2LYN0MN+Gk3CN3y0122uTLI41z8l62BFhnxS/ylVkZT2D1PLdmLTu5itV1iHh9SF+kDomY3DrZig== -"@types/react-dom@^16.9.8": - version "16.9.8" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" - integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== +"@types/react-dom@^17.0.11": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466" + integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q== dependencies: "@types/react" "*" "@types/react-transition-group@^4.2.0": - version "4.4.0" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.0.tgz#882839db465df1320e4753e6e9f70ca7e9b4d46d" - integrity sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w== + version "4.4.4" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" + integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.51": - version "16.9.51" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.51.tgz#f8aa51ffa9996f1387f63686696d9b59713d2b60" - integrity sha512-lQa12IyO+DMlnSZ3+AGHRUiUcpK47aakMMoBG8f7HGxJT8Yfe+WE128HIXaHOHVPReAW0oDS3KAI0JI2DDe1PQ== +"@types/react@*", "@types/react@^17.0.37": + version "17.0.37" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" + integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== dependencies: "@types/prop-types" "*" + "@types/scheduler" "*" csstype "^3.0.2" +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + "@types/selenium-webdriver@^3.0.0": - version "3.0.17" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b" - integrity sha512-tGomyEuzSC1H28y2zlW6XPCaDaXFaD6soTdb4GNdmte2qfHtrKqhy0ZFs4r/1hpazCfEZqeTSRLvSasmEx89uw== + version "3.0.19" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.19.tgz#28ecede76f15b13553b4e86074d4cf9a0bbe49c4" + integrity sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g== "@types/sizzle@*": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" - integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== "@types/source-list-map@*": version "0.1.2" @@ -1919,176 +2010,157 @@ integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== "@types/styled-jsx@^2.2.8": - version "2.2.8" - resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" - integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== + version "2.2.9" + resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.9.tgz#e50b3f868c055bcbf9bc353eca6c10fdad32a53f" + integrity sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw== dependencies: "@types/react" "*" -"@types/tinycolor2@^1.4.2": - version "1.4.2" - resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf" - integrity sha512-PeHg/AtdW6aaIO2a+98Xj7rWY4KC1E6yOy7AFknJQ7VXUGNrMlyxDFxJo7HqLtjQms/ZhhQX52mLVW/EX3JGOw== +"@types/systemjs@6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.1.1.tgz#eae17f2a080e867d01a2dd614f524ab227cf5a41" + integrity sha512-d1M6eDKBGWx7RbYy295VEFoOF9YDJkPI959QYnmzcmeaV+SP4D0xV7dEh3sN5XF3GvO3PhGzm+17Z598nvHQuQ== -"@types/tooltipster@^0.0.30": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/tooltipster/-/tooltipster-0.0.30.tgz#10deda04d1ce52e0fefc18079ae91548663e2400" - integrity sha512-JqgcjpkDOMjsiVg3tqMIDgxnealN9/+DY6+z9OMeSd4fRovHINr/tHKtBJXDHI2DKh+ygyqMHsPTct+flNS+bw== +"@types/tinycolor2@^1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.3.tgz#ed4a0901f954b126e6a914b4839c77462d56e706" + integrity sha512-Kf1w9NE5HEgGxCRyIcRXR/ZYtDv0V8FVPtYHwLxl0O+maGX0erE77pQlD0gpP+/KByMZ87mOA79SjifhSB3PjQ== + +"@types/tooltipster@^0.0.31": + version "0.0.31" + resolved "https://registry.yarnpkg.com/@types/tooltipster/-/tooltipster-0.0.31.tgz#db6c78b5ad709fe5dc9c78cf15a6a2068b4f72b0" + integrity sha512-tDAxe2Q67VoQyeEW6oweNDfw4nNmodFGkHdPQdeBCCusf2d3qFbDLFkYnntgSwcD00Fkhh8mSguaP6w5muvZpg== dependencies: "@types/jquery" "*" "@types/webpack-sources@^0.1.5": - version "0.1.8" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.8.tgz#078d75410435993ec8a0a2855e88706f3f751f81" - integrity sha512-JHB2/xZlXOjzjBB6fMOpH1eQAfsrpqVVIbneE0Rok16WXwFaznaI5vfg75U5WgGJm7V9W1c4xeRQDjX/zwvghA== + version "0.1.9" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.9.tgz#da69b06eb34f6432e6658acb5a6893c55d983920" + integrity sha512-bvzMnzqoK16PQIC8AYHNdW45eREJQMd6WG/msQWX5V2+vZmODCOPb4TJcbgRljTZZTwTM4wUMcsI8FftNA7new== dependencies: "@types/node" "*" "@types/source-list-map" "*" source-map "^0.6.1" -"@webassemblyjs/ast@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" - integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== - dependencies: - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" - -"@webassemblyjs/floating-point-hex-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" - integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== - -"@webassemblyjs/helper-api-error@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" - integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== - -"@webassemblyjs/helper-buffer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" - integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== - -"@webassemblyjs/helper-code-frame@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" - integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== - dependencies: - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/helper-fsm@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" - integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== - -"@webassemblyjs/helper-module-context@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" - integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== - dependencies: - "@webassemblyjs/ast" "1.9.0" - -"@webassemblyjs/helper-wasm-bytecode@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" - integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== - -"@webassemblyjs/helper-wasm-section@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" - integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - -"@webassemblyjs/ieee754@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" - integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== dependencies: - "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== -"@webassemblyjs/leb128@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" - integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" - integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== - -"@webassemblyjs/wasm-edit@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" - integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/helper-wasm-section" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-opt" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - "@webassemblyjs/wast-printer" "1.9.0" - -"@webassemblyjs/wasm-gen@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" - integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wasm-opt@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" - integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-buffer" "1.9.0" - "@webassemblyjs/wasm-gen" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - -"@webassemblyjs/wasm-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" - integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-wasm-bytecode" "1.9.0" - "@webassemblyjs/ieee754" "1.9.0" - "@webassemblyjs/leb128" "1.9.0" - "@webassemblyjs/utf8" "1.9.0" - -"@webassemblyjs/wast-parser@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" - integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/floating-point-hex-parser" "1.9.0" - "@webassemblyjs/helper-api-error" "1.9.0" - "@webassemblyjs/helper-code-frame" "1.9.0" - "@webassemblyjs/helper-fsm" "1.9.0" - "@xtuc/long" "4.2.2" +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/wast-printer@1.9.0": - version "1.9.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" - integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/wast-parser" "1.9.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -2124,22 +2196,25 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" -ace-builds@^1.4.12, ace-builds@^1.4.6: - version "1.4.12" - resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.12.tgz#888efa386e36f4345f40b5233fcc4fe4c588fae7" - integrity sha512-G+chJctFPiiLGvs3+/Mly3apXTcfgE45dT5yp12BcWZ1kUs+gm0qd3/fv4gsz6fVag4mM0moHVpjHDIgph6Psg== +ace-builds@^1.4.13: + version "1.4.13" + resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.4.13.tgz#186f42d3849ebcc6a48b93088a058489897514c1" + integrity sha512-SOLzdaQkY6ecPKYRDDg+MY1WoGgXA34cIvYJNNoBMGGUswHmlauU2Hy0UL96vW0Fs/LgFbMUjD+6vqzWTldIYQ== -acorn@^6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" - integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -add-dom-event-listener@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310" - integrity sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw== - dependencies: - object-assign "4.x" +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.6.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" + integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== adjust-sourcemap-loader@^4.0.0: version "4.0.0" @@ -2154,7 +2229,7 @@ adm-zip@^0.4.9: resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.16.tgz#cf4c508fdffab02c269cbc7f471a875f05570365" integrity sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg== -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -2190,12 +2265,43 @@ ajv-errors@^1.0.0: resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== -ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: +ajv-formats@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.0.tgz#96eaf83e38d32108b66d82a9cb0cfa24886cdfeb" + integrity sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q== + dependencies: + ajv "^8.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.1.0, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@6.12.6, ajv@^6.12.5: +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@8.6.2: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" + integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.1.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2205,14 +2311,14 @@ ajv@6.12.6, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: - version "6.12.5" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== +ajv@^8.0.0, ajv@^8.8.0: + version "8.8.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb" + integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw== dependencies: fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" uri-js "^4.2.2" alphanum-sort@^1.0.2: @@ -2220,20 +2326,21 @@ alphanum-sort@^1.0.2: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= -angular-gridster2@~11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/angular-gridster2/-/angular-gridster2-11.2.0.tgz#57239e639d16b7b1bcd6255aa8fee55151dd65d7" - integrity sha512-9+4Qt2/mPKAzGhhzp2Ag++WQbvr3Ry3dbfHG1XI596jpKh1ZtYvTLrh0Fn6jJEq248F1VN0xob9K9PzTH4P/2A== +angular-gridster2@~12.1.1: + version "12.1.1" + resolved "https://registry.yarnpkg.com/angular-gridster2/-/angular-gridster2-12.1.1.tgz#699cd0a2477b81b052f6bd7b336ba7cc2adc8bc3" + integrity sha512-HK7vf212LSn7mp8g4sNA6/X8TQN4wJyHupKemx+PUPmTs6FHDyquUMUFXYxANR47jdyLEMW/DxCDI0bEhIIChw== dependencies: - tslib "^2.0.0" + tslib "^2.1.0" -angular2-hotkeys@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/angular2-hotkeys/-/angular2-hotkeys-2.2.0.tgz#7b52ba99c42c56656360953ab79d776f583d1ab8" - integrity sha512-2O2wtPyscQU/PtyPc+TefSHAql0VI51rrKyIt87YAvBaGUZEj5PZG2QtC7kYI3sFhXYlvrNefUxXoehFjEVQAQ== +angular2-hotkeys@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/angular2-hotkeys/-/angular2-hotkeys-2.4.0.tgz#b814ccc43bb55eddff103d2ccf42afd832e3ff9d" + integrity sha512-m1UHmPBCKNUoGDPbyfLxUcUXd7vn1welfpfCLv7hEM2HIYfEtFo158xx7l4mAufEeE5179uFJggSQcVwJennfg== dependencies: "@types/mousetrap" "^1.6.0" mousetrap "^1.6.0" + tslib "^2.0.0" ansi-colors@4.1.1: version "4.1.1" @@ -2246,11 +2353,11 @@ ansi-colors@^3.0.0: integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== ansi-escapes@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" - integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== dependencies: - type-fest "^0.11.0" + type-fest "^0.21.3" ansi-html@0.0.7: version "0.0.7" @@ -2262,20 +2369,15 @@ ansi-regex@^2.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-styles@^2.2.1: version "2.2.1" @@ -2296,6 +2398,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansidec@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/ansidec/-/ansidec-0.3.4.tgz#e12d267d6b1f122d2da5b98fe0de1f98d14ac62b" + integrity sha512-Ydgbey4zqUmmNN2i2OVeVHXig3PxHRbok2X6B2Sogmb92JzZUFfTL806dT7os6tBL1peXItfeFt76CP3zsoXUg== + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2304,10 +2411,10 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" -anymatch@~3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" - integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -2317,15 +2424,15 @@ app-root-path@^3.0.0: resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad" integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw== -aproba@^1.0.3, aproba@^1.1.1: +aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + version "1.1.7" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== dependencies: delegates "^1.0.0" readable-stream "^2.0.6" @@ -2400,22 +2507,12 @@ array-unique@^0.3.2: arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= - -asn1.js@^5.2.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" @@ -2424,14 +2521,6 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -assert@^1.1.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" - integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== - dependencies: - object-assign "^4.1.1" - util "0.10.3" - assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" @@ -2469,21 +2558,22 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -attr-accept@^2.0.0: +attr-accept@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== -autoprefixer@10.2.4: - version "10.2.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.2.4.tgz#c0e7cf24fcc6a1ae5d6250c623f0cb8beef2f7e1" - integrity sha512-DCCdUQiMD+P/as8m3XkeTUkUKuuRqLGcwD0nll7wevhqoJfMRpJlkFd1+MQh1pvupjiQuip42lc/VFvfUTMSKw== +autoprefixer@^9.6.1: + version "9.8.8" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" + integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== dependencies: - browserslist "^4.16.1" - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - fraction.js "^4.0.13" + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" normalize-range "^0.1.2" + num2fraction "^1.2.2" + picocolors "^0.2.1" + postcss "^7.0.32" postcss-value-parser "^4.1.0" aws-sign2@~0.7.0: @@ -2492,9 +2582,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428" - integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axobject-query@2.0.2: version "2.0.2" @@ -2520,27 +2610,46 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +babel-plugin-polyfill-corejs2@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.3.tgz#6ed8e30981b062f8fe6aca8873a37ebcc8cc1c0f" + integrity sha512-NDZ0auNRzmAfE1oDDPW2JhzIMXUk+FFe2ICejmt5T4ocKgiQx3e0VCRx9NCAidcMtL2RUZaWtXnmjTCkx0tcbA== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.2.4" + semver "^6.1.1" -base64-arraybuffer@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812" - integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI= +babel-plugin-polyfill-corejs3@^0.2.2: + version "0.2.5" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.5.tgz#2779846a16a1652244ae268b1e906ada107faf92" + integrity sha512-ninF5MQNwAX9Z7c9ED+H2pGt1mXdP4TqzlHKyPIYmJIYz0N+++uwdM7RnJukklhzJ54Q84vA4ZJkgs7lu5vqcw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.2" + core-js-compat "^3.16.2" + +babel-plugin-polyfill-regenerator@^0.2.2: + version "0.2.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.3.tgz#2e9808f5027c4336c994992b48a4262580cb8d6d" + integrity sha512-JVE78oRZPKFIeUqFGrSORNzQnrDwZR16oiWeGM8ZyjBn2XAT5OjP+wXx5ESuo33nUsFUEJYjtklnsKbxW5L+7g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.2.4" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-arraybuffer@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz#4b944fac0191aa5907afe2d8c999ccc57ce80f45" integrity sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ== -base64-js@^1.0.2: - version "1.3.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" - integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== +base64-arraybuffer@^1.0.1, base64-arraybuffer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c" + integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA== -base64-js@^1.3.1: +base64-js@^1.2.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2586,9 +2695,9 @@ binary-extensions@^1.0.0: integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== binary-extensions@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" - integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== bindings@^1.5.0: version "1.5.0" @@ -2597,7 +2706,7 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bl@^4.0.3: +bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== @@ -2613,21 +2722,6 @@ blocking-proxy@^1.0.0: dependencies: minimist "^1.2.0" -bluebird@^3.5.5: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: - version "4.11.9" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" - integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== - -bn.js@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" - integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== - body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -2692,143 +2786,35 @@ braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= - -browserify-aes@^1.0.0, browserify-aes@^1.0.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" - integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== - dependencies: - buffer-xor "^1.0.3" - cipher-base "^1.0.0" - create-hash "^1.1.0" - evp_bytestokey "^1.0.3" - inherits "^2.0.1" - safe-buffer "^5.0.1" - -browserify-cipher@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" - integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== - dependencies: - browserify-aes "^1.0.4" - browserify-des "^1.0.0" - evp_bytestokey "^1.0.0" - -browserify-des@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" - integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== - dependencies: - cipher-base "^1.0.1" - des.js "^1.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" - integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= - dependencies: - bn.js "^4.1.0" - randombytes "^2.0.1" - -browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== - dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" - create-hash "^1.2.0" - create-hmac "^1.1.7" - elliptic "^6.5.3" - inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -browserify-zlib@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" - integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== - dependencies: - pako "~1.0.5" - -browserslist@^4.0.0, browserslist@^4.9.1: - version "4.14.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.14.5.tgz#1c751461a102ddc60e40993639b709be7f2c4015" - integrity sha512-Z+vsCZIvCBvqLoYkBFTwEYH3v5MCQbsAjp50ERycpOjnPmolg1Gjy4+KaWWpm8QOJt9GHkhdqAl14NpCX73CWA== - dependencies: - caniuse-lite "^1.0.30001135" - electron-to-chromium "^1.3.571" - escalade "^3.1.0" - node-releases "^1.1.61" - -browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.1, browserslist@^4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.6.4, browserslist@^4.9.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" + integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== dependencies: - caniuse-lite "^1.0.30001219" - colorette "^1.2.2" - electron-to-chromium "^1.3.723" + caniuse-lite "^1.0.30001280" + electron-to-chromium "^1.3.896" escalade "^3.1.1" - node-releases "^1.1.71" + node-releases "^2.0.1" + picocolors "^1.0.0" browserstack@^1.5.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.0.tgz#5a56ab90987605d9c138d7a8b88128370297f9bf" - integrity sha512-HJDJ0TSlmkwnt9RZ+v5gFpa1XZTBYTj0ywvLwJ3241J7vMw2jAsGNVhKHtmCOyg+VxeLZyaibO9UL71AsUeDIw== + version "1.6.1" + resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.6.1.tgz#e051f9733ec3b507659f395c7a4765a1b1e358b3" + integrity sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw== dependencies: https-proxy-agent "^2.2.1" -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - -buffer-from@^1.0.0, buffer-from@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== buffer-indexof@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== -buffer-xor@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" - integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= - -buffer@^4.3.0: - version "4.9.2" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" - integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== - dependencies: - base64-js "^1.0.2" - ieee754 "^1.1.4" - isarray "^1.0.0" - -buffer@^5.4.3, buffer@^5.5.0: +buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -2841,11 +2827,6 @@ builtin-modules@^1.1.1: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= -builtin-status-codes@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" - integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= - builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -2861,10 +2842,10 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@15.0.5, cacache@^15.0.5: - version "15.0.5" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" - integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== +cacache@15.2.0: + version "15.2.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.2.0.tgz#73af75f77c58e72d8c630a7a2858cb18ef523389" + integrity sha512-uKoJSHmnrqXgthDFx/IU6ED/5xd+NNGe+Bb+kLZy7Ku4P+BaiWEUflAKPZ7eAzsYGcsAGASJZsybXp+quEcHTw== dependencies: "@npmcli/move-file" "^1.0.1" chownr "^2.0.0" @@ -2880,30 +2861,33 @@ cacache@15.0.5, cacache@^15.0.5: p-map "^4.0.0" promise-inflight "^1.0.1" rimraf "^3.0.2" - ssri "^8.0.0" + ssri "^8.0.1" tar "^6.0.2" unique-filename "^1.1.1" -cacache@^12.0.2: - version "12.0.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" - integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== +cacache@^15.0.5, cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== dependencies: - bluebird "^3.5.5" - chownr "^1.1.1" - figgy-pudding "^3.5.1" + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" glob "^7.1.4" - graceful-fs "^4.1.15" - infer-owner "^1.0.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - mkdirp "^0.5.1" - move-concurrently "^1.0.1" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" promise-inflight "^1.0.1" - rimraf "^2.6.3" - ssri "^6.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" unique-filename "^1.1.1" - y18n "^4.0.0" cache-base@^1.0.1: version "1.0.1" @@ -2920,6 +2904,14 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -2930,11 +2922,6 @@ camelcase@^5.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" - integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== - caniuse-api@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" @@ -2945,15 +2932,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001135: - version "1.0.30001146" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001146.tgz#c61fcb1474520c1462913689201fb292ba6f447c" - integrity sha512-VAy5RHDfTJhpxnDdp2n40GPPLp3KqNrXz1QqFv4J64HvArKs8nuNMOWkB3ICOaBTU/Aj4rYAo/ytdQDDFF/Pug== - -caniuse-lite@^1.0.30001181, caniuse-lite@^1.0.30001219: - version "1.0.30001228" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz#bfdc5942cd3326fa51ee0b42fbef4da9d492a7fa" - integrity sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001280: + version "1.0.30001284" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001284.tgz#d3653929ded898cd0c1f09a56fd8ca6952df4fca" + integrity sha512-t28SKa7g6kiIQi6NHeOcKrOrGMzCRrXvlasPwWC26TH2QNdglgzQIRUuJ0cR3NeQPH+5jpuveeeSFDLm2zbkEw== canonical-path@1.0.0: version "1.0.0" @@ -2990,10 +2972,10 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== +chalk@^4.1.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -3003,20 +2985,20 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -"chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.4.1: - version "3.4.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" - integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.4.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.1.2" + fsevents "~2.3.2" chokidar@^2.1.8: version "2.1.8" @@ -3037,51 +3019,21 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.2: - version "3.5.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== - dependencies: - anymatch "~3.1.1" - braces "~3.0.2" - glob-parent "~5.1.0" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.5.0" - optionalDependencies: - fsevents "~2.3.1" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - chownr@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== chrome-trace-event@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" - integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== - dependencies: - tslib "^1.9.0" + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" - integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - circular-dependency-plugin@5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz#39e836079db1d3cf2f988dc48c5188a44058b600" @@ -3098,9 +3050,9 @@ class-utils@^0.3.5: static-extend "^0.1.1" classnames@2.x, classnames@^2.2.1, classnames@^2.2.6: - version "2.2.6" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" - integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q== + version "2.3.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" + integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== clean-stack@^2.0.0: version "2.2.0" @@ -3115,24 +3067,15 @@ cli-cursor@^3.1.0: restore-cursor "^3.1.0" cli-spinners@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939" - integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q== + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== cli-width@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== -clipboard@^2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376" - integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3236,20 +3179,15 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colord@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.0.0.tgz#f8c19f2526b7dc5b22d6e57ef102f03a2a43a3d8" - integrity sha512-WMDFJfoY3wqPZNpKUFdse3HhD5BHCbE9JCdxRzoVH+ywRITGOeWAHNkGEmyxLlErEpN9OLMWgdM9dWQtDk5dog== - -colorette@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" - integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colord@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e" + integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw== colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== colors@1.4.0, colors@^1.4.0: version "1.4.0" @@ -3268,26 +3206,21 @@ commander@^2.11.0, commander@^2.12.1, commander@^2.19.0, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^6.0.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -commander@^7.1.0: +commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== +commander@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compass-sass-mixins@^0.12.7: - version "0.12.7" - resolved "https://registry.yarnpkg.com/compass-sass-mixins/-/compass-sass-mixins-0.12.7.tgz#2ac4d310f2ebe518b7d6aca4ae24f1d325409e8c" - integrity sha1-KsTTEPLr5Ri31qykriTx0yVAnow= - component-emitter@^1.2.1, component-emitter@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" @@ -3300,16 +3233,13 @@ compressible@~2.0.16: dependencies: mime-db ">= 1.43.0 < 2" -compression-webpack-plugin@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-6.1.1.tgz#ae8e4b2ffdb7396bb776e66918d751a20d8ccf0e" - integrity sha512-BEHft9M6lwOqVIQFMS/YJGmeCYXVOakC5KzQk05TFpMBlODByh1qNsZCWjUBxCQhUP9x0WfGidxTbGkjbWO/TQ== +compression-webpack-plugin@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-9.0.1.tgz#fd80350670ca88cc8a1c0edac40ee92a0e06fcd4" + integrity sha512-vqlhZIPSyCpy6eaYWy8iPhteLWpARKotRiN5B/jr7lLowJv1GVc98Snn1Dcxe0+SKbfydLu7qZcnNuP+AyG19Q== dependencies: - cacache "^15.0.5" - find-cache-dir "^3.3.1" - schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - webpack-sources "^1.4.3" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" compression@^1.7.4: version "1.7.4" @@ -3329,20 +3259,10 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@^1.5.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - config-chain@^1.1.12: - version "1.1.12" - resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" - integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== + version "1.1.13" + resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== dependencies: ini "^1.3.4" proto-list "~1.2.1" @@ -3362,21 +3282,11 @@ connect@^3.7.0: parseurl "~1.3.3" utils-merge "1.0.1" -console-browserify@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" - integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== - console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -constants-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" - integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= - content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -3390,9 +3300,9 @@ content-type@~1.0.4: integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== convert-source-map@^1.5.1, convert-source-map@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" - integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + version "1.8.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== dependencies: safe-buffer "~5.1.1" @@ -3418,63 +3328,52 @@ copy-anything@^2.0.1: dependencies: is-what "^3.12.0" -copy-concurrently@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" - integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== - dependencies: - aproba "^1.1.1" - fs-write-stream-atomic "^1.0.8" - iferr "^0.1.5" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.0" - copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -copy-webpack-plugin@6.3.2: - version "6.3.2" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.3.2.tgz#0e920a6c181a5052aa6e2861b164bda03f83afeb" - integrity sha512-MgJ1uouLIbDg4ST1GzqrGQyKoXY5iPqi6fghFqarijam7FQcBa/r6Rg0VkoIuzx75Xq8iAMghyOueMkWUQ5OaA== +copy-webpack-plugin@9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-9.0.1.tgz#b71d21991599f61a4ee00ba79087b8ba279bbb59" + integrity sha512-14gHKKdYIxF84jCEgPgYXCPpldbwpxxLbCmA7LReY7gvbaT555DgeBWBgBZM116tv/fO6RRJrsivBqRyRlukhw== dependencies: - cacache "^15.0.5" - fast-glob "^3.2.4" - find-cache-dir "^3.3.1" - glob-parent "^5.1.1" - globby "^11.0.1" - loader-utils "^2.0.0" + fast-glob "^3.2.5" + glob-parent "^6.0.0" + globby "^11.0.3" normalize-path "^3.0.0" - p-limit "^3.0.2" + p-limit "^3.1.0" schema-utils "^3.0.0" - serialize-javascript "^5.0.1" - webpack-sources "^1.4.3" + serialize-javascript "^6.0.0" -core-js-compat@^3.8.0: - version "3.12.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.12.1.tgz#2c302c4708505fa7072b0adb5156d26f7801a18b" - integrity sha512-i6h5qODpw6EsHAoIdQhKoZdWn+dGBF3dSS8m5tif36RlWvW3A6+yu2S16QHUo3CrkzrnEskMAt9f8FxmY9fhWQ== +core-js-compat@^3.15.0, core-js-compat@^3.16.2: + version "3.19.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.2.tgz#18066a3404a302433cb0aa8be82dd3d75c76e5c4" + integrity sha512-ObBY1W5vx/LFFMaL1P5Udo4Npib6fu+cMokeziWkA8Tns4FcDemKF5j9JvaI5JhdkW8EQJQGJN1EcrzmEwuAqQ== dependencies: - browserslist "^4.16.6" + browserslist "^4.18.1" semver "7.0.0" -core-js@3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0" - integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== +core-js@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.16.0.tgz#1d46fb33720bc1fa7f90d20431f36a5540858986" + integrity sha512-5+5VxRFmSf97nM8Jr2wzOwLqRo6zphH2aX+7KsAUONObyzakDNq2G/bgbhinxB4PoV9L3aXQYhiDKyIKWd2c8g== -core-js@^3.12.1: - version "3.12.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.12.1.tgz#6b5af4ff55616c08a44d386f1f510917ff204112" - integrity sha512-Ne9DKPHTObRuB09Dru5AjwKjY4cJHVGu+y5f7coGn1E9Grkc3p2iBwE9AI/nJzsE29mQF7oq+mhYYRqOMFN1Bw== +core-js@^3.19.2: + version "3.19.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.2.tgz#ae216d7f4f7e924d9a2e3ff1e4b1940220f9157b" + integrity sha512-ciYCResnLIATSsXuXnIOH4CbdfgV+H1Ltg16hJFN7/v6OxqnFr/IFGeLacaZ+fHLAm0TBbXwNK9/DNBzBUrO/g== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -3484,9 +3383,9 @@ cors@~2.8.5: vary "^1" cosmiconfig@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.0.tgz#ef9b44d773959cae63ddecd122de23853b60f8d3" - integrity sha512-pondGvTuVYDk++upghXJabWzL6Kxu6f26ljFw64Swq9v6sQPUL3EUlVDV56diOjpCayKihL6hVe8exIACU4XcA== + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" import-fresh "^3.2.1" @@ -3494,46 +3393,21 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -create-ecdh@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" - integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== - dependencies: - bn.js "^4.1.0" - elliptic "^6.5.3" - -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" - integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== - dependencies: - cipher-base "^1.0.1" - inherits "^2.0.1" - md5.js "^1.3.4" - ripemd160 "^2.0.1" - sha.js "^2.4.0" - -create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" - integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== - dependencies: - cipher-base "^1.0.3" - create-hash "^1.1.0" - inherits "^2.0.1" - ripemd160 "^2.0.0" - safe-buffer "^5.0.1" - sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -critters@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.7.tgz#548b470360f4f3c51e622de3b7aa733c8f0b17bf" - integrity sha512-qUF2SaAWFYjNPdCcPpu68p2DnHiosia84yx5mPTlUMQjkjChR+n6sO1/I7yn2U2qNDgSPTd2SoaTIDQcUL+EwQ== +critters@0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/critters/-/critters-0.0.12.tgz#32baa87526e053a41b67e19921673ed92264e2ab" + integrity sha512-ujxKtKc/mWpjrOKeaACTaQ1aP0O31M0ZPWhfl85jZF1smPU4Ivb9va5Ox2poif4zVJQQo0LCFlzGtEZAsCAPcw== dependencies: chalk "^4.1.0" - css "^3.0.0" + css-select "^4.1.3" parse5 "^6.0.1" parse5-htmlparser2-tree-adapter "^6.0.1" + postcss "^8.3.7" pretty-bytes "^5.3.0" cross-spawn@^6.0.0, cross-spawn@^6.0.5: @@ -3547,64 +3421,61 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -crypto-browserify@^3.11.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" - integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== - dependencies: - browserify-cipher "^1.0.0" - browserify-sign "^4.0.0" - create-ecdh "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.0" - diffie-hellman "^5.0.0" - inherits "^2.0.1" - pbkdf2 "^3.0.3" - public-encrypt "^4.0.0" - randombytes "^2.0.0" - randomfill "^1.0.3" - -css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-color-names@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67" - integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA== +css-blank-pseudo@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" + integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w== + dependencies: + postcss "^7.0.5" css-declaration-sorter@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.0.3.tgz#9dfd8ea0df4cc7846827876fafb52314890c21a9" - integrity sha512-52P95mvW1SMzuRZegvpluT6yEv0FqQusydKQPZsNN5Q7hh8EwQvN8E2nwuJ16BBvNN6LcoIZXu/Bk58DAhrrxw== + version "6.1.3" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz#e9852e4cf940ba79f509d9425b137d1f94438dc2" + integrity sha512-SvjQjNRZgh4ULK1LDJ2AduPKUKxIqmtU7ZAyi47BTV+M90Qvxr9AB6lKlLbDUfXqI9IQeYA8LbAsCZPpJEV3aA== dependencies: timsort "^0.3.0" -css-line-break@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-1.1.1.tgz#d5e9bdd297840099eb0503c7310fd34927a026ef" - integrity sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA== +css-has-pseudo@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee" + integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^5.0.0-rc.4" + +css-line-break@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.0.1.tgz#3dc74c2ed5eb64211480281932475790243e7338" + integrity sha512-gwKYIMUn7xodIcb346wgUhE2Dt5O1Kmrc16PWi8sL4FTfyDj8P5095rzH7+O8CTZudJr+uw2GCI/hwEkDJFI2w== dependencies: base64-arraybuffer "^0.2.0" -css-loader@5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.1.tgz#9e4de0d6636a6266a585bd0900b422c85539d25f" - integrity sha512-cXc2ti9V234cq7rJzFKhirb2L2iPy8ZjALeVJAozXYz9te3r4eqLSixNAbMDJSgJEQywqXzs8gonxaboeKqwiw== +css-loader@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.2.0.tgz#9663d9443841de957a3cb9bcea2eda65b3377071" + integrity sha512-/rvHfYRjIpymZblf49w8jYcRo2y9gj6rV8UroHGmBxKrIyGLokpycyKzp9OkitvqT29ZSpzJ0Ic7SpnJX3sC8g== dependencies: - camelcase "^6.2.0" - cssesc "^3.0.0" - icss-utils "^5.0.0" - loader-utils "^2.0.0" - postcss "^8.1.4" + icss-utils "^5.1.0" + postcss "^8.2.15" postcss-modules-extract-imports "^3.0.0" postcss-modules-local-by-default "^4.0.0" postcss-modules-scope "^3.0.0" postcss-modules-values "^4.0.0" postcss-value-parser "^4.1.0" + semver "^7.3.5" + +css-minimizer-webpack-plugin@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#8fadbdf10128cb40227bff275a4bb47412534245" + integrity sha512-B3I5e17RwvKPJwsxjjWcdgpU/zqylzK1bPVghcmpFHRL48DXiBgrtqz1BJsn68+t/zzaLp9kYAaEDvQ7GyanFQ== + dependencies: + cssnano "^5.0.6" + jest-worker "^27.0.2" + p-limit "^3.0.2" + postcss "^8.3.5" schema-utils "^3.0.0" - semver "^7.3.2" + serialize-javascript "^6.0.0" + source-map "^0.6.1" css-parse@~2.0.0: version "2.0.0" @@ -3613,15 +3484,22 @@ css-parse@~2.0.0: dependencies: css "^2.0.0" -css-select@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-3.1.2.tgz#d52cbdc6fee379fba97fb0d3925abbd18af2d9d8" - integrity sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA== +css-prefers-color-scheme@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4" + integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg== + dependencies: + postcss "^7.0.5" + +css-select@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067" + integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA== dependencies: boolbase "^1.0.0" - css-what "^4.0.0" - domhandler "^4.0.0" - domutils "^2.4.3" + css-what "^5.0.0" + domhandler "^4.2.0" + domutils "^2.6.0" nth-check "^2.0.0" css-selector-tokenizer@^0.7.1: @@ -3632,7 +3510,7 @@ css-selector-tokenizer@^0.7.1: cssesc "^3.0.0" fastparse "^1.1.2" -css-tree@^1.1.2: +css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== @@ -3648,10 +3526,10 @@ css-vendor@^2.0.8: "@babel/runtime" "^7.8.3" is-in-browser "^1.0.2" -css-what@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-4.0.0.tgz#35e73761cab2eeb3d3661126b23d7aa0e8432233" - integrity sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A== +css-what@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.1.0.tgz#3f7b707aadf633baf62c2ceb8579b545bb40f7fe" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== css@^2.0.0: version "2.2.4" @@ -3663,15 +3541,6 @@ css@^2.0.0: source-map-resolve "^0.5.2" urix "^0.1.0" -css@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" - integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== - dependencies: - inherits "^2.0.4" - source-map "^0.6.1" - source-map-resolve "^0.6.0" - cssauron@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/cssauron/-/cssauron-1.4.0.tgz#a6602dff7e04a8306dc0db9a551e92e8b5662ad8" @@ -3679,30 +3548,40 @@ cssauron@^1.4.0: dependencies: through X.X.X +cssdb@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" + integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ== + +cssesc@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" + integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -cssnano-preset-default@^5.0.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.1.tgz#5cd783caed942cc94159aeb10583af4691445b8c" - integrity sha512-kAhR71Tascmnjlhl4UegGA3KGGbMLXHkkqVpA9idsRT1JmIhIsz1C3tDpBeQMUw5EX5Rfb1HGc/PRqD2AFk3Vg== +cssnano-preset-default@^5.1.8: + version "5.1.8" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.8.tgz#7525feb1b72f7b06e57f55064cbdae341d79dea2" + integrity sha512-zWMlP0+AMPBVE852SqTrP0DnhTcTA2C1wAF92TKZ3Va+aUVqLIhkqKlnJIXXdqXD7RN+S1ujuWmNpvrJBiM/vg== dependencies: css-declaration-sorter "^6.0.3" cssnano-utils "^2.0.1" postcss-calc "^8.0.0" - postcss-colormin "^5.1.1" - postcss-convert-values "^5.0.1" + postcss-colormin "^5.2.1" + postcss-convert-values "^5.0.2" postcss-discard-comments "^5.0.1" postcss-discard-duplicates "^5.0.1" postcss-discard-empty "^5.0.1" postcss-discard-overridden "^5.0.1" - postcss-merge-longhand "^5.0.2" - postcss-merge-rules "^5.0.1" + postcss-merge-longhand "^5.0.4" + postcss-merge-rules "^5.0.3" postcss-minify-font-values "^5.0.1" - postcss-minify-gradients "^5.0.1" - postcss-minify-params "^5.0.1" + postcss-minify-gradients "^5.0.3" + postcss-minify-params "^5.0.2" postcss-minify-selectors "^5.1.0" postcss-normalize-charset "^5.0.1" postcss-normalize-display-values "^5.0.1" @@ -3711,27 +3590,28 @@ cssnano-preset-default@^5.0.1: postcss-normalize-string "^5.0.1" postcss-normalize-timing-functions "^5.0.1" postcss-normalize-unicode "^5.0.1" - postcss-normalize-url "^5.0.1" + postcss-normalize-url "^5.0.3" postcss-normalize-whitespace "^5.0.1" - postcss-ordered-values "^5.0.1" - postcss-reduce-initial "^5.0.1" + postcss-ordered-values "^5.0.2" + postcss-reduce-initial "^5.0.2" postcss-reduce-transforms "^5.0.1" - postcss-svgo "^5.0.1" - postcss-unique-selectors "^5.0.1" + postcss-svgo "^5.0.3" + postcss-unique-selectors "^5.0.2" cssnano-utils@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== -cssnano@5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.2.tgz#3f6de4fd5ecb7b5fb636c1a606de5f38cd241493" - integrity sha512-8JK3EnPsjQsULme9/e5M2hF564f/480hwsdcHvQ7ZtAIMfQ1O3SCfs+b8Mjf5KJxhYApyRshR2QSovEJi2K72Q== +cssnano@^5.0.6: + version "5.0.12" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.12.tgz#2c083a1c786fc9dc2d5522bd3c0e331b7cd302ab" + integrity sha512-U38V4x2iJ3ijPdeWqUrEr4eKBB5PbEKsNP5T8xcik2Au3LeMtiMHX0i2Hu9k51FcKofNZumbrcdC6+a521IUHg== dependencies: - cosmiconfig "^7.0.0" - cssnano-preset-default "^5.0.1" + cssnano-preset-default "^5.1.8" is-resolvable "^1.1.0" + lilconfig "^2.0.3" + yaml "^1.10.2" csso@^4.2.0: version "4.2.0" @@ -3741,29 +3621,24 @@ csso@^4.2.0: css-tree "^1.1.2" csstype@^2.5.2: - version "2.6.13" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.13.tgz#a6893015b90e84dd6e85d0e3b442a1e84f2dbe0f" - integrity sha512-ul26pfSQTZW8dcOnD2iiJssfXw0gdNVX9IJDH/X3K5DGPfj+fUYe3kB+swUY6BF3oZDxaID3AJt+9/ojSAE05A== + version "2.6.19" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa" + integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ== csstype@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" - integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== + version "3.0.10" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= -cyclist@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" - integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= - damerau-levenshtein@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" - integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== + version "1.0.7" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" + integrity sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw== dashdash@^1.12.0: version "1.14.1" @@ -3772,10 +3647,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.21.3: - version "2.21.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.21.3.tgz#8f5f6889d7a96bbcc1f0ea50239b397a83357f9b" - integrity sha512-HeYdzCaFflc1i4tGbj7JKMjM4cKGYoyxwcIIkHzNgCkX8xXDNJDZXgDDVchIWpN4eQc3lH37WarduXFZJOtxfw== +date-fns@^2.26.0: + version "2.27.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.27.0.tgz#e1ff3c3ddbbab8a2eaadbb6106be2929a5a2d92b" + integrity sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q== date-format@^2.1.0: version "2.1.0" @@ -3787,6 +3662,11 @@ date-format@^3.0.0: resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95" integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w== +dayjs@^1.10.4: + version "1.10.7" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" + integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3794,34 +3674,27 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@4.3.1, debug@~4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@~4.3.1, debug@~4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.1.1: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +debug@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: - ms "^2.1.1" + ms "2.1.2" -debug@^3.2.6: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== - dependencies: - ms "2.1.2" - debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -3871,6 +3744,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" @@ -3931,11 +3809,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -3946,18 +3819,10 @@ depd@^1.1.2, depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -dependency-graph@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.7.2.tgz#91db9de6eb72699209d88aea4c1fd5221cac1c49" - integrity sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ== - -des.js@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" - integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== - dependencies: - inherits "^2.0.1" - minimalistic-assert "^1.0.0" +dependency-graph@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27" + integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== destroy@~1.0.4: version "1.0.4" @@ -3965,16 +3830,16 @@ destroy@~1.0.4: integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= detect-node@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" - integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= -diff-match-patch@^1.0.4: +diff-match-patch@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== @@ -3984,15 +3849,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diffie-hellman@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" - integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== - dependencies: - bn.js "^4.1.0" - miller-rabin "^4.0.0" - randombytes "^2.0.0" - dijkstrajs@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.2.tgz#2e48c0d3b825462afe75ab4ad5e829c8ece36257" @@ -4005,10 +3861,10 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -directory-tree@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/directory-tree/-/directory-tree-2.2.4.tgz#6d5bd7d82e48378e256a1e87b678a43c50076e2e" - integrity sha512-2N43msQptKbi3WMfIs+U09yi6bfyKL+MWyj5VMj8t1F/Tx04bt1cn/EEIU3o1JBltlJk7NQnzOEuTNa/KQvbWA== +directory-tree@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/directory-tree/-/directory-tree-3.0.1.tgz#ce7e087d7a9a58947ab802125b4c45481270db72" + integrity sha512-IJL5L/zd73xbchfsIKMrhaRJCt28ek1UsS6LNZ6b9r7m+VbU1fd7JZAypn6gl1g4hpxn21QF3/scAWKIOMk4Ug== dns-equal@^1.0.0: version "1.0.0" @@ -4016,9 +3872,9 @@ dns-equal@^1.0.0: integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= dns-packet@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" - integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== dependencies: ip "^1.1.0" safe-buffer "^5.0.1" @@ -4031,14 +3887,14 @@ dns-txt@^2.0.2: buffer-indexof "^1.0.0" dom-align@^1.7.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.0.tgz#56fb7156df0b91099830364d2d48f88963f5a29c" - integrity sha512-YkoezQuhp3SLFGdOlr5xkqZ640iXrnHAwVYcDg8ZKRUtO7mSzSC2BA5V0VuyAwPSJA4CLIc6EDDJh4bEsD2+zA== + version "1.12.2" + resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.2.tgz#0f8164ebd0c9c21b0c790310493cd855892acd4b" + integrity sha512-pHuazgqrsTFrGU2WLDdXxCFabkdQDx72ddkraZNih1KsMcN5qsRSTR9O4VJRlwTPCPb5COYg3LOfiMHHcPInHg== dom-helpers@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b" - integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ== + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== dependencies: "@babel/runtime" "^7.8.7" csstype "^3.0.2" @@ -4059,50 +3915,30 @@ dom-serializer@^1.0.1: integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== dependencies: domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -domain-browser@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" - integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== - -domelementtype@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.2.tgz#f3b6e549201e46f588b59463dd77187131fe6971" - integrity sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA== + domhandler "^4.2.0" + entities "^2.0.0" -domelementtype@^2.2.0: +domelementtype@^2.0.1, domelementtype@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== -domhandler@^4.0.0, domhandler@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" - integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== +domhandler@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.0.tgz#16c658c626cf966967e306f966b431f77d4a5626" + integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== dependencies: domelementtype "^2.2.0" -domutils@^2.4.3: - version "2.6.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" - integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== +domutils@^2.6.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== dependencies: dom-serializer "^1.0.1" domelementtype "^2.2.0" domhandler "^4.2.0" -duplexify@^3.4.2, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -4126,28 +3962,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.571: - version "1.3.578" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.578.tgz#e6671936f4571a874eb26e2e833aa0b2c0b776e0" - integrity sha512-z4gU6dA1CbBJsAErW5swTGAaU2TBzc2mPAonJb00zqW1rOraDo2zfBMDRvaz9cVic+0JEZiYbHWPw/fTaZlG2Q== - -electron-to-chromium@^1.3.723: - version "1.3.737" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.737.tgz#196f2e9656f4f3c31930750e1899c091b72d36b5" - integrity sha512-P/B84AgUSQXaum7a8m11HUsYL8tj9h/Pt5f7Hg7Ty6bm5DxlFq+e5+ouHUoNQMsKDJ7u4yGfI8mOErCmSH9wyg== - -elliptic@^6.5.3: - version "6.5.3" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" - integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== - dependencies: - bn.js "^4.4.0" - brorand "^1.0.1" - hash.js "^1.0.0" - hmac-drbg "^1.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.0" +electron-to-chromium@^1.3.896: + version "1.4.10" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.10.tgz#5f44ae6f6725b1949d6e8d34352f80d4c1880734" + integrity sha512-tFgA40Iq2oy4k2PnZrLJowbgpij+lD6ZLxkw8Ht1NKTYyN8dvSvC5xlo8X0WW2jqhKSzITrbr5mpB4/AZ/8OUA== emoji-regex@^7.0.1: version "7.0.3" @@ -4159,7 +3977,7 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-toolkit@^6.0.1: +emoji-toolkit@^6.5.0: version "6.6.0" resolved "https://registry.yarnpkg.com/emoji-toolkit/-/emoji-toolkit-6.6.0.tgz#e7287c43a96f940ec4c5428cd7100a40e57518f1" integrity sha512-pEu0kow2p1N8zCKnn/L6H0F3rWUBB3P3hVjr/O5yl1fK7N9jU4vO4G7EFapC5Y3XwZLUCY0FZbOPyTkH+4V2eQ== @@ -4169,6 +3987,11 @@ emojis-list@^3.0.0: resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== +encode-utf8@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/encode-utf8/-/encode-utf8-1.0.3.tgz#f30fdd31da07fb596f281beb2f6b027851994cda" + integrity sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -4181,88 +4004,68 @@ encoding@^0.1.12: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.0.0, end-of-stream@^1.1.0: +end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" -engine.io-parser@~4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.2.tgz#e41d0b3fb66f7bf4a3671d2038a154024edb501e" - integrity sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg== +engine.io-parser@~5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.2.tgz#69a2ec3ed431da021f0666712d07f106bcffa6ce" + integrity sha512-wuiO7qO/OEkPJSFueuATIXtrxF7/6GTbAO9QLv7nnbjwZ5tYhLm9zxvLwxstRs0dcT0KUlWTjtIOs1T86jt12g== dependencies: - base64-arraybuffer "0.1.4" + base64-arraybuffer "~1.0.1" -engine.io@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b" - integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w== +engine.io@~6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.0.tgz#459eab0c3724899d7b63a20c3a6835cf92857939" + integrity sha512-ErhZOVu2xweCjEfYcTdkCnEYUiZgkAcBBAhW4jbIvNG8SLU3orAqoJCiytZjYF7eTpVmmCrLDjLIEaPlUAs1uw== dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "2.0.0" cookie "~0.4.1" cors "~2.8.5" debug "~4.3.1" - engine.io-parser "~4.0.0" - ws "~7.4.2" + engine.io-parser "~5.0.0" + ws "~8.2.3" -enhanced-resolve@5.7.0: - version "5.7.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.7.0.tgz#525c5d856680fbd5052de453ac83e32049958b5c" - integrity sha512-6njwt/NsZFUKhM6j9U8hzVyD4E4r0x7NQzhTCbcWOJ0IQjNSAoalWmb0AE51Wn+fwan5qVESWi7t2ToBxs9vrw== +enhanced-resolve@^5.8.0, enhanced-resolve@^5.8.3: + version "5.8.3" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" + integrity sha512-EGAbGvH7j7Xt2nc0E7D99La1OiEs8LnyimkRgwExpUMScN6O+3x9tIWs7PLQZVNx4YD+00skHXPXi1yQHpAmZA== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" -enhanced-resolve@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz#3b806f3bfafc1ec7de69551ef93cca46c1704126" - integrity sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -enhanced-resolve@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" - integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - ent@~2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= entities@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" - integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== env-paths@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== -err-code@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" - integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= - err-code@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== -errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== +errno@^0.1.1, errno@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== dependencies: prr "~1.0.1" @@ -4273,49 +4076,15 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" +es-module-lexer@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d" + integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw== -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== es6-promise@^4.0.3: version "4.2.8" @@ -4329,10 +4098,118 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -escalade@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" - integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== +esbuild-android-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.8.tgz#c20e875c3c98164b1ffba9b28637bdf96f5e9e7c" + integrity sha512-AilbChndywpk7CdKkNSZ9klxl+9MboLctXd9LwLo3b0dawmOF/i/t2U5d8LM6SbT1Xw36F8yngSUPrd8yPs2RA== + +esbuild-darwin-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.8.tgz#f46e6b471ddbf62265234808a6a1aa91df18a417" + integrity sha512-b6sdiT84zV5LVaoF+UoMVGJzR/iE2vNUfUDfFQGrm4LBwM/PWXweKpuu6RD9mcyCq18cLxkP6w/LD/w9DtX3ng== + +esbuild-darwin-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.8.tgz#a991157a6013facd4f2e14159b7da52626c90154" + integrity sha512-R8YuPiiJayuJJRUBG4H0VwkEKo6AvhJs2m7Tl0JaIer3u1FHHXwGhMxjJDmK+kXwTFPriSysPvcobXC/UrrZCQ== + +esbuild-freebsd-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.8.tgz#301601d2e443ad458960e359b402a17d9500be9d" + integrity sha512-zBn6urrn8FnKC+YSgDxdof9jhPCeU8kR/qaamlV4gI8R3KUaUK162WYM7UyFVAlj9N0MyD3AtB+hltzu4cysTw== + +esbuild-freebsd-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.8.tgz#039a63acc12ec0892006c147ea221e55f9125a9f" + integrity sha512-pWW2slN7lGlkx0MOEBoUGwRX5UgSCLq3dy2c8RIOpiHtA87xAUpDBvZK10MykbT+aMfXc0NI2lu1X+6kI34xng== + +esbuild-linux-32@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.8.tgz#c537b67d7e694b60bfa2786581412838c6ba0284" + integrity sha512-T0I0ueeKVO/Is0CAeSEOG9s2jeNNb8jrrMwG9QBIm3UU18MRB60ERgkS2uV3fZ1vP2F8i3Z2e3Zju4lg9dhVmw== + +esbuild-linux-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.8.tgz#0092fc8a064001a777bfa0e3b425bb8be8f96e6a" + integrity sha512-Bm8SYmFtvfDCIu9sjKppFXzRXn2BVpuCinU1ChTuMtdKI/7aPpXIrkqBNOgPTOQO9AylJJc1Zw6EvtKORhn64w== + +esbuild-linux-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.8.tgz#5cd3f2bb924212971482e8dbc25c4afd09b28110" + integrity sha512-X4pWZ+SL+FJ09chWFgRNO3F+YtvAQRcWh0uxKqZSWKiWodAB20flsW/OWFYLXBKiVCTeoGMvENZS/GeVac7+tQ== + +esbuild-linux-arm@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.8.tgz#ad634f96bf2975536907aeb9fdb75a3194f4ddce" + integrity sha512-4/HfcC40LJ4GPyboHA+db0jpFarTB628D1ifU+/5bunIgY+t6mHkJWyxWxAAE8wl/ZIuRYB9RJFdYpu1AXGPdg== + +esbuild-linux-mips64le@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.8.tgz#57857edfebf9bf65766dc8be1637f2179c990572" + integrity sha512-o7e0D+sqHKT31v+mwFircJFjwSKVd2nbkHEn4l9xQ1hLR+Bv8rnt3HqlblY3+sBdlrOTGSwz0ReROlKUMJyldA== + +esbuild-linux-ppc64le@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.8.tgz#fdb82a059a5b86bb10fb42091b4ebcf488b9cd46" + integrity sha512-eZSQ0ERsWkukJp2px/UWJHVNuy0lMoz/HZcRWAbB6reoaBw7S9vMzYNUnflfL3XA6WDs+dZn3ekHE4Y2uWLGig== + +esbuild-netbsd-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.8.tgz#d7879e7123d3b2c04754ece8bd061aa6866deeff" + integrity sha512-gZX4kP7gVvOrvX0ZwgHmbuHczQUwqYppxqtoyC7VNd80t5nBHOFXVhWo2Ad/Lms0E8b+wwgI/WjZFTCpUHOg9Q== + +esbuild-openbsd-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.8.tgz#88b280b6cb0a3f6adb60abf27fc506c506a35cf0" + integrity sha512-afzza308X4WmcebexbTzAgfEWt9MUkdTvwIa8xOu4CM2qGbl2LanqEl8/LUs8jh6Gqw6WsicEK52GPrS9wvkcw== + +esbuild-sunos-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.8.tgz#229ae7c7703196a58acd0f0291ad9bebda815d63" + integrity sha512-mWPZibmBbuMKD+LDN23LGcOZ2EawMYBONMXXHmbuxeT0XxCNwadbCVwUQ/2p5Dp5Kvf6mhrlIffcnWOiCBpiVw== + +esbuild-wasm@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.13.8.tgz#f34134c187ffcfc22d476e925917f70bab40f8b0" + integrity sha512-UbD+3nloiSpJWXTCInZQrqPe8Y+RLfDkY/5kEHiXsw/lmaEvibe69qTzQu16m5R9je/0bF7VYQ5jaEOq0z9lLA== + +esbuild-windows-32@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.8.tgz#892d093e32a21c0c9135e5a0ffdc380aeb70e763" + integrity sha512-QsZ1HnWIcnIEApETZWw8HlOhDSWqdZX2SylU7IzGxOYyVcX7QI06ety/aDcn437mwyO7Ph4RrbhB+2ntM8kX8A== + +esbuild-windows-64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.8.tgz#7defd8d79ae3bb7e6f53b65a7190be7daf901686" + integrity sha512-76Fb57B9eE/JmJi1QmUW0tRLQZfGo0it+JeYoCDTSlbTn7LV44ecOHIMJSSgZADUtRMWT9z0Kz186bnaB3amSg== + +esbuild-windows-arm64@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.8.tgz#e59ae004496fd8a5ab67bfc7945a2e47480d6fb9" + integrity sha512-HW6Mtq5eTudllxY2YgT62MrVcn7oq2o8TAoAvDUhyiEmRmDY8tPwAhb1vxw5/cdkbukM3KdMYtksnUhF/ekWeg== + +esbuild@0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.8.tgz#bd7cc51b881ab067789f88e17baca74724c1ec4f" + integrity sha512-A4af7G7YZLfG5OnARJRMtlpEsCkq/zHZQXewgPA864l9D6VjjbH1SuFYK/OSV6BtHwDGkdwyRrX0qQFLnMfUcw== + optionalDependencies: + esbuild-android-arm64 "0.13.8" + esbuild-darwin-64 "0.13.8" + esbuild-darwin-arm64 "0.13.8" + esbuild-freebsd-64 "0.13.8" + esbuild-freebsd-arm64 "0.13.8" + esbuild-linux-32 "0.13.8" + esbuild-linux-64 "0.13.8" + esbuild-linux-arm "0.13.8" + esbuild-linux-arm64 "0.13.8" + esbuild-linux-mips64le "0.13.8" + esbuild-linux-ppc64le "0.13.8" + esbuild-netbsd-64 "0.13.8" + esbuild-openbsd-64 "0.13.8" + esbuild-sunos-64 "0.13.8" + esbuild-windows-32 "0.13.8" + esbuild-windows-64 "0.13.8" + esbuild-windows-arm64 "0.13.8" escalade@^3.1.1: version "3.1.1" @@ -4349,12 +4226,12 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -eslint-scope@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" - integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== dependencies: - esrecurse "^4.1.0" + esrecurse "^4.3.0" estraverse "^4.1.1" esprima@^4.0.0: @@ -4362,7 +4239,7 @@ esprima@^4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esrecurse@^4.1.0: +esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== @@ -4375,9 +4252,9 @@ estraverse@^4.1.1: integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== estraverse@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" - integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== esutils@^2.0.2: version "2.0.3" @@ -4394,31 +4271,28 @@ eve-raphael@0.5.0: resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA= +eventemitter-asyncresource@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" + integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" - integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" - integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + version "1.1.0" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf" + integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg== dependencies: original "^1.0.0" -evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" - integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== - dependencies: - md5.js "^1.3.4" - safe-buffer "^5.1.1" - execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -4535,26 +4409,25 @@ extsprintf@1.3.0: integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.1.1, fast-glob@^3.2.4: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== +fast-glob@^3.1.1, fast-glob@^3.2.5: + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.0" + glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.2" - picomatch "^2.2.1" + micromatch "^4.0.4" fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@^2.0.0: version "2.1.0" @@ -4567,9 +4440,9 @@ fastparse@^1.1.2: integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ== fastq@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" - integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== dependencies: reusify "^1.0.4" @@ -4580,11 +4453,6 @@ faye-websocket@^0.11.3: dependencies: websocket-driver ">=0.5.1" -figgy-pudding@^3.5.1: - version "3.5.2" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== - figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -4592,20 +4460,12 @@ figures@^3.0.0: dependencies: escape-string-regexp "^1.0.5" -file-loader@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" - integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - -file-selector@^0.1.12: - version "0.1.13" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.13.tgz#5efd977ca2bca1700992df1b10e254f4e73d2df4" - integrity sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ== +file-selector@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80" + integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA== dependencies: - tslib "^2.0.1" + tslib "^2.0.3" file-uri-to-path@1.0.0: version "1.0.0" @@ -4642,7 +4502,7 @@ finalhandler@1.1.2, finalhandler@~1.1.2: statuses "~1.5.0" unpipe "~1.0.0" -find-cache-dir@3.3.1, find-cache-dir@^3.3.1: +find-cache-dir@3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== @@ -4651,14 +4511,14 @@ find-cache-dir@3.3.1, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-cache-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== dependencies: commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" + make-dir "^3.0.2" + pkg-dir "^4.1.0" find-up@^3.0.0: version "3.0.0" @@ -4687,6 +4547,11 @@ flatted@^2.0.1: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatten@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" + integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== + "flot.curvedlines@git://github.com/MichaelZinsmaier/CurvedLines.git#master": version "1.1.1" resolved "git://github.com/MichaelZinsmaier/CurvedLines.git#22ed1fc2a6ccafc816c2d07b36027cc123825c4b" @@ -4695,18 +4560,10 @@ flatted@^2.0.1: version "0.9.0-alpha" resolved "git://github.com/thingsboard/flot.git#0ff0c775db7c74e705f6c3c2bba92080a202ccd4" -flush-write-stream@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" - integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== - dependencies: - inherits "^2.0.3" - readable-stream "^2.3.6" - follow-redirects@^1.0.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" - integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== + version "1.14.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" + integrity sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA== font-awesome@^4.7.0: version "4.7.0" @@ -4732,15 +4589,10 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= - -fraction.js@^4.0.13: - version "4.1.1" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.1.tgz#ac4e520473dae67012d618aab91eda09bcb400ff" - integrity sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg== +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fragment-cache@^0.2.1: version "0.2.1" @@ -4754,23 +4606,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -from2@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" - integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= - dependencies: - inherits "^2.0.1" - readable-stream "^2.0.0" - -fs-extra@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" - integrity sha1-+RcExT0bRh+JNFKwwwfZmXZHq2s= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^4.0.0" - universalify "^0.1.0" - fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -4796,15 +4631,10 @@ fs-minipass@^2.0.0, fs-minipass@^2.1.0: dependencies: minipass "^3.0.0" -fs-write-stream-atomic@^1.0.8: - version "1.0.10" - resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" - integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= - dependencies: - graceful-fs "^4.1.2" - iferr "^0.1.5" - imurmurhash "^0.1.4" - readable-stream "1 || 2" +fs-monkey@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== fs.realpath@^1.0.0: version "1.0.0" @@ -4819,12 +4649,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@~2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" - integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== - -fsevents@~2.3.1: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -4848,21 +4673,36 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" -gensync@^1.0.0-beta.1: - version "1.0.0-beta.1" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" - integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +geojson-rbush@3.x: + version "3.2.0" + resolved "https://registry.yarnpkg.com/geojson-rbush/-/geojson-rbush-3.2.0.tgz#8b543cf0d56f99b78faf1da52bb66acad6dfc290" + integrity sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w== + dependencies: + "@turf/bbox" "*" + "@turf/helpers" "6.x" + "@turf/meta" "6.x" + "@types/geojson" "7946.0.8" + rbush "^3.0.1" + get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-intrinsic@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -4890,17 +4730,41 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" - integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== dependencies: is-glob "^4.0.1" -glob@7.1.6, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== +glob-parent@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.1.7: + version "7.1.7" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" + integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -4914,10 +4778,10 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globby@^11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== +globby@^11.0.3: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -4949,22 +4813,10 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= - dependencies: - delegate "^3.1.2" - -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== - -graceful-fs@^4.2.3: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.8" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" + integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== hammerjs@^2.0.8: version "2.0.8" @@ -5006,10 +4858,17 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" has-unicode@^2.0.0: version "2.0.1" @@ -5054,36 +4913,19 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hash-base@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" - integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== - dependencies: - inherits "^2.0.4" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" - -hash.js@^1.0.0, hash.js@^1.0.3: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== +hdr-histogram-js@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.1.tgz#ecb1ff2bcb6181c3e93ff4af9472c28c7e97284e" + integrity sha512-uPZxl1dAFnjUFHWLZmt93vUUvtHeaBay9nVNHu38SdOjMSF/4KqJUqa1Seuj08ptU1rEb6AHvB41X8n/zFZ74Q== dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" + "@assemblyscript/loader" "^0.10.1" + base64-js "^1.2.0" + pako "^1.0.3" -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - -hmac-drbg@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" +hdr-histogram-percentiles-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" + integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== hoist-non-react-statics@^3.3.2: version "3.3.2" @@ -5092,20 +4934,6 @@ hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" -hosted-git-info@^3.0.2: - version "3.0.5" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.5.tgz#bea87905ef7317442e8df3087faa3c842397df03" - integrity sha512-i4dpK6xj9BIpVOTboXIlKG9+8HMKggcrMX7WA24xZtKwX0TPelq/rbaS5rCKeNX8sJXZJGdSxpnEGtta+wismQ== - dependencies: - lru-cache "^6.0.0" - -hosted-git-info@^3.0.6: - version "3.0.8" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" - integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== - dependencies: - lru-cache "^6.0.0" - hosted-git-info@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961" @@ -5123,32 +4951,23 @@ hpack.js@^2.1.6: readable-stream "^2.0.1" wbuf "^1.1.0" -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - html-entities@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" - integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== + version "1.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" + integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -html2canvas@^1.0.0-rc.7: - version "1.0.0-rc.7" - resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98" - integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA== +html2canvas@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.3.3.tgz#f01531f9bf7c73a771c5ee73b4a5cab6576bf876" + integrity sha512-nQi0ayEY1cMiUMbq/F5hRwMAqsRMo7NIP6VaCqaXnXO6b/FfZO49oSfIJjdyRha28EuY8D6FBCzQOXPQV0TCrA== dependencies: - css-line-break "1.1.1" + css-line-break "2.0.1" + text-segmentation "^1.0.2" http-cache-semantics@^4.1.0: version "4.1.0" @@ -5193,9 +5012,9 @@ http-errors@~1.7.2: toidentifier "1.0.0" http-parser-js@>=0.5.1: - version "0.5.2" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" - integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== + version "0.5.5" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.5.tgz#d7c30d5d3c90d865b4a2e870181f9d6f22ac7ac5" + integrity sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA== http-proxy-agent@^4.0.1: version "4.0.1" @@ -5234,11 +5053,6 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -https-browserify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" - integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= - https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -5274,14 +5088,14 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" - integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -icss-utils@^5.0.0: +icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== @@ -5291,16 +5105,6 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ieee754@^1.1.4: - version "1.1.13" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" - integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== - -iferr@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" - integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= - ignore-walk@^3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" @@ -5309,9 +5113,9 @@ ignore-walk@^3.0.3: minimatch "^3.0.4" ignore@^5.1.4: - version "5.1.8" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" - integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + version "5.1.9" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" + integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== image-size@~0.5.0: version "0.5.5" @@ -5344,13 +5148,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indefinite-observable@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/indefinite-observable/-/indefinite-observable-2.0.1.tgz#574af29bfbc17eb5947793797bddc94c9d859400" - integrity sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ== - dependencies: - symbol-observable "1.2.0" - indent-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" @@ -5361,7 +5158,7 @@ indexes-of@^1.0.1: resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= -infer-owner@^1.0.3, infer-owner@^1.0.4: +infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== @@ -5374,16 +5171,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" - integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= - inherits@2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -5395,25 +5187,26 @@ ini@2.0.0: integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== ini@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -inquirer@7.3.3: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== +inquirer@8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.2.tgz#65b204d2cd7fb63400edd925dfe428bafd422e3d" + integrity sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q== dependencies: ansi-escapes "^4.2.1" - chalk "^4.1.0" + chalk "^4.1.1" cli-cursor "^3.1.0" cli-width "^3.0.0" external-editor "^3.0.3" figures "^3.0.0" - lodash "^4.17.19" + lodash "^4.17.21" mute-stream "0.0.8" + ora "^5.3.0" run-async "^2.4.0" - rxjs "^6.6.0" + rxjs "^7.2.0" string-width "^4.1.0" strip-ansi "^6.0.0" through "^2.3.6" @@ -5461,9 +5254,12 @@ is-accessor-descriptor@^1.0.0: kind-of "^6.0.0" is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-arrayish@^0.2.1: version "0.2.1" @@ -5489,11 +5285,6 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== - is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -5501,22 +5292,10 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-color-stop@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - -is-core-module@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" - integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== +is-core-module@^2.2.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" + integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== dependencies: has "^1.0.3" @@ -5535,9 +5314,11 @@ is-data-descriptor@^1.0.0: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" is-descriptor@^0.1.0: version "0.1.6" @@ -5557,10 +5338,10 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-docker@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.1.1.tgz#4125a88e44e450d384e09047ede71adc2d144156" - integrity sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw== +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -5603,10 +5384,10 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -5625,11 +5406,6 @@ is-lambda@^1.0.1: resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -5687,12 +5463,13 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-regex@^1.0.4, is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== +is-regex@^1.0.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== dependencies: - has-symbols "^1.0.1" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" is-resolvable@^1.1.0: version "1.1.0" @@ -5704,18 +5481,16 @@ is-stream@^1.1.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= -is-symbol@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-what@^3.12.0: version "3.14.1" resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" @@ -5731,27 +5506,22 @@ is-wsl@^1.1.0: resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is-wsl@^2.1.1: +is-wsl@^2.1.1, is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== dependencies: is-docker "^2.0.0" -isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isbinaryfile@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" - integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== +isbinaryfile@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf" + integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w== isexe@^2.0.0: version "2.0.0" @@ -5781,9 +5551,9 @@ istanbul-lib-coverage@^2.0.5: integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== istanbul-lib-coverage@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec" - integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg== + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== istanbul-lib-instrument@^4.0.3: version "4.0.3" @@ -5816,28 +5586,23 @@ istanbul-lib-source-maps@^3.0.6: source-map "^0.6.1" istanbul-reports@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b" - integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw== + version "3.1.1" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.1.tgz#7085857f17d2441053c6ce5c3b8fdf6882289397" + integrity sha512-q1kvhAXWSsXfMjCdNHNPKZZv94OlspKnoGv+R9RGbnqOOQ0VbNfLFgQDVgi7hHenKsndGq3/o0OBdzDXthWcNw== dependencies: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jasmine-core@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" - integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw== +jasmine-core@^3.6.0, jasmine-core@~3.10.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.10.1.tgz#7aa6fa2b834a522315c651a128d940eca553989a" + integrity sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA== jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= -jasmine-core@~3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561" - integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ== - jasmine-spec-reporter@~7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-7.0.0.tgz#94b939448e63d4e2bd01668142389f20f0a8ea49" @@ -5859,44 +5624,43 @@ jasminewd2@^2.1.0: resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= -jest-worker@26.6.2, jest-worker@^26.5.0: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" - integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== +jest-worker@^27.0.2, jest-worker@^27.0.6: + version "27.4.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.2.tgz#0fb123d50955af1a450267787f340a1bf7e12bc4" + integrity sha512-0QMy/zPovLfUPyHuOuuU4E+kGACXXE84nRnq6lBVI9GJg5DCBiA97SATi+ZP8CpiJwEQy1oCPjRBf8AnLjN+Ag== dependencies: "@types/node" "*" merge-stream "^2.0.0" - supports-color "^7.0.0" + supports-color "^8.0.0" -jquery.terminal@^2.24.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/jquery.terminal/-/jquery.terminal-2.24.0.tgz#779a92b4478ac7542e7adaf5e17f4d4d92fd622c" - integrity sha512-rtjTBj7/FYoBiWawiNTYNXIRqVI4n39WF/x1h/bEKFb48Rzgr4/dpkKGd1dgZau714ewzczAi7CEBTKvhM6RjA== +jquery.terminal@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/jquery.terminal/-/jquery.terminal-2.29.4.tgz#0403942e3f68a23d54ba9ad7c592f1da2b2829cd" + integrity sha512-ugzctBbbJmw3ZGIhYziBFEK8QnWAZy2q6sZUtmPgjni4ILg59tO0WJI9DkKiDZ4LQtdCVexQ5oP0J90jMN+TzA== dependencies: - "@types/jquery" "^3.5.5" + "@jcubic/lily" "^0.3.0" + "@types/jquery" "^3.5.6" + ansidec "^0.3.4" + iconv-lite "^0.6.3" jquery "^3.6.0" - prismjs "^1.23.0" + prismjs "^1.25.0" wcwidth "^1.0.1" + optionalDependencies: + fsevents "^2.3.2" -jquery@>=1.9.1, jquery@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" - integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== - -jquery@^3.6.0: +jquery@>=1.9.1, jquery@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== -js-beautify@^1.13.13: - version "1.13.13" - resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.13.13.tgz#756907d1728f329f2b84c42efd56ad17514620bf" - integrity sha512-oH+nc0U5mOAqX8M5JO1J0Pw/7Q35sAdOsM5W3i87pir9Ntx6P/5Gx1xLNoK+MGyvHk4rqqRCE4Oq58H6xl2W7A== +js-beautify@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d" + integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ== dependencies: config-chain "^1.1.12" editorconfig "^0.15.3" glob "^7.1.3" - mkdirp "^1.0.4" nopt "^5.0.0" "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: @@ -5905,9 +5669,9 @@ js-beautify@^1.13.13: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: - version "3.14.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" - integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -5949,10 +5713,15 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stringify-safe@~5.0.1: version "5.0.1" @@ -5972,9 +5741,9 @@ json5@^1.0.1: minimist "^1.2.0" json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== dependencies: minimist "^1.2.5" @@ -5996,83 +5765,82 @@ jsonparse@^1.3.1: integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== dependencies: assert-plus "1.0.0" extsprintf "1.3.0" - json-schema "0.2.3" + json-schema "0.4.0" verror "1.10.0" jss-plugin-camel-case@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz#93d2cd704bf0c4af70cc40fb52d74b8a2554b170" - integrity sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.8.2.tgz#8d7f915c8115afaff8cbde08faf610ec9892fba6" + integrity sha512-2INyxR+1UdNuKf4v9It3tNfPvf7IPrtkiwzofeKuMd5D58/dxDJVUQYRVg/n460rTlHUfsEQx43hDrcxi9dSPA== dependencies: "@babel/runtime" "^7.3.1" hyphenate-style-name "^1.0.3" - jss "10.6.0" + jss "10.8.2" jss-plugin-default-unit@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz#af47972486819b375f0f3a9e0213403a84b5ef3b" - integrity sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.8.2.tgz#c66f12e02e0815d911b85c02c2a979ee7b4ce69a" + integrity sha512-UZ7cwT9NFYSG+SEy7noRU50s4zifulFdjkUNKE+u6mW7vFP960+RglWjTgMfh79G6OENZmaYnjHV/gcKV4nSxg== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.8.2" jss-plugin-global@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz#3e8011f760f399cbadcca7f10a485b729c50e3ed" - integrity sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.8.2.tgz#1a35632a693cf50113bcc5ffe6b51969df79c4ec" + integrity sha512-UaYMSPsYZ7s/ECGoj4KoHC2jwQd5iQ7K+FFGnCAILdQrv7hPmvM2Ydg45ThT/sH46DqktCRV2SqjRuxeBH8nRA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.8.2" jss-plugin-nested@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz#5f83c5c337d3b38004834e8426957715a0251641" - integrity sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.8.2.tgz#79f3c7f75ea6a36ae72fe52e777035bb24d230c7" + integrity sha512-acRvuPJOb930fuYmhkJaa994EADpt8TxI63Iyg96C8FJ9T2xRyU5T6R1IYKRwUiqZo+2Sr7fdGzRTDD4uBZaMA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.8.2" tiny-warning "^1.0.2" jss-plugin-props-sort@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz#297879f35f9fe21196448579fee37bcde28ce6bc" - integrity sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.8.2.tgz#e25a7471868652c394562b6dc5433dcaea7dff6f" + integrity sha512-wqdcjayKRWBZnNpLUrXvsWqh+5J5YToAQ+8HNBNw0kZxVvCDwzhK2Nx6AKs7p+5/MbAh2PLgNW5Ym/ysbVAuqQ== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.8.2" jss-plugin-rule-value-function@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz#3c1a557236a139d0151e70a82c810ccce1c1c5ea" - integrity sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.8.2.tgz#55354b55f1b2968a15976729968f767f02d64049" + integrity sha512-bW0EKAs+0HXpb6BKJhrn94IDdiWb0CnSluTkh0rGEgyzY/nmD1uV/Wf6KGlesGOZ9gmJzQy+9FFdxIUID1c9Ug== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.8.2" tiny-warning "^1.0.2" jss-plugin-vendor-prefixer@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz#e1fcd499352846890c38085b11dbd7aa1c4f2c78" - integrity sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ== + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.8.2.tgz#ebb4a482642f34091e454901e21176441dd5f475" + integrity sha512-DeGv18QsSiYLSVIEB2+l0af6OToUe0JB+trpzUxyqD2QRC/5AzzDrCrYffO5AHZ81QbffYvSN/pkfZaTWpRXlg== dependencies: "@babel/runtime" "^7.3.1" css-vendor "^2.0.8" - jss "10.6.0" + jss "10.8.2" -jss@10.6.0, jss@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.6.0.tgz#d92ff9d0f214f65ca1718591b68e107be4774149" - integrity sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw== +jss@10.8.2, jss@^10.5.1: + version "10.8.2" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.8.2.tgz#4b2a30b094b924629a64928236017a52c7c97505" + integrity sha512-FkoUNxI329CKQ9OQC8L72MBF9KPf5q8mIupAJ5twU7G7XREW7ahb+7jFfrjZ4iy1qvhx1HwIWUIvkZBDnKkEdQ== dependencies: "@babel/runtime" "^7.3.1" csstype "^3.0.2" - indefinite-observable "^2.0.1" is-in-browser "^1.1.3" tiny-warning "^1.0.2" @@ -6083,27 +5851,17 @@ jstree-bootstrap-theme@^1.0.1: dependencies: jquery ">=1.9.1" -jstree@^3.3.11: - version "3.3.11" - resolved "https://registry.yarnpkg.com/jstree/-/jstree-3.3.11.tgz#da2f12bcab6af61839586c81db46e8f2e19160aa" - integrity sha512-9ZJKroPjCyjb6JLPuAbBrLJKT6pS1f4m5gkwoEagG5oQWtvzm0IiDsntXTxeFtz7AmqrKfij+gLfF9MgWriNxg== +jstree@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/jstree/-/jstree-3.3.12.tgz#cf206bc85dcf4a4664ed6617eaae3bd5983d8601" + integrity sha512-vHNLWkUr02ZYH7RcIckvhtLUtneWCVEtIKpIp2G9WtRh01ITv18EoNtNQcFG3ozM+oK6wp1Z300gSLXNQWCqGA== dependencies: jquery ">=1.9.1" -jszip@^3.1.3: - version "3.5.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.5.0.tgz#b4fd1f368245346658e781fec9675802489e15f6" - integrity sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - set-immediate-shim "~1.0.1" - -jszip@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" - integrity sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ== +jszip@^3.1.3, jszip@^3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" + integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== dependencies: lie "~3.3.0" pako "~1.0.2" @@ -6128,12 +5886,12 @@ karma-coverage-istanbul-reporter@~3.0.3: istanbul-reports "^3.0.2" minimatch "^3.0.4" -karma-jasmine-html-reporter@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.6.0.tgz#586e17025a1b4128e9fba55d5f1e8921bfc3bc1e" - integrity sha512-ELO9yf0cNqpzaNLsfFgXd/wxZVYkE2+ECUwhMHUD4PZ17kcsPsYsVyjquiRqyMn2jkd2sHt0IeMyAyq1MC23Fw== +karma-jasmine-html-reporter@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz#52c489a74d760934a1089bfa5ea4a8fcb84cc28b" + integrity sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ== -karma-jasmine@~4.0.0: +karma-jasmine@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82" integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw== @@ -6147,41 +5905,41 @@ karma-source-map-support@1.4.0: dependencies: source-map-support "^0.5.5" -karma@~6.3.2: - version "6.3.2" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.2.tgz#24b62fbae3e8b5218cc32a0dac49ad08a541e76d" - integrity sha512-fo4Wt0S99/8vylZMxNj4cBFyOBBnC1bewZ0QOlePij/2SZVWxqbyLeIddY13q6URa2EpLRW8ixvFRUMjkmo1bw== +karma@~6.3.9: + version "6.3.9" + resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.9.tgz#cc309607f0fcdb58a88643184f3e4ba8bff26751" + integrity sha512-E/MqdLM9uVIhfuyVnrhlGBu4miafBdXEAEqCmwdEMh3n17C7UWC/8Kvm3AYKr91gc7scutekZ0xv6rxRaUCtnw== dependencies: body-parser "^1.19.0" braces "^3.0.2" - chokidar "^3.4.2" + chokidar "^3.5.1" colors "^1.4.0" connect "^3.7.0" di "^0.0.1" dom-serialize "^2.2.1" - glob "^7.1.6" - graceful-fs "^4.2.4" + glob "^7.1.7" + graceful-fs "^4.2.6" http-proxy "^1.18.1" - isbinaryfile "^4.0.6" - lodash "^4.17.19" - log4js "^6.2.1" - mime "^2.4.5" + isbinaryfile "^4.0.8" + lodash "^4.17.21" + log4js "^6.3.0" + mime "^2.5.2" minimatch "^3.0.4" qjobs "^1.2.0" range-parser "^1.2.1" rimraf "^3.0.2" - socket.io "^3.1.0" + socket.io "^4.2.0" source-map "^0.6.1" - tmp "0.2.1" - ua-parser-js "^0.7.23" + tmp "^0.2.1" + ua-parser-js "^0.7.30" yargs "^16.1.1" katex@^0.13.0: - version "0.13.18" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.18.tgz#ba89e8e4b70cc2325e25e019a62b9fe71e5c2931" - integrity sha512-a3dC4NSVSDU3O1WZbTnOiA8rVNJ2lSiomOl0kmckCIGObccIHXof7gAseIY0o1gjEspe+34ZeSEX2D1ChFKIvA== + version "0.13.24" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.13.24.tgz#fe55455eb455698cb24b911a353d16a3c855d905" + integrity sha512-jZxYuKCma3VS5UuxOx/rFV1QyGSl3Uy/i0kTJF3HgQ5xMinCQVF8Zd4bMY/9aI9b9A2pjIBOsjSSm68ykTAr8w== dependencies: - commander "^6.0.0" + commander "^8.0.0" killable@^1.0.1: version "1.0.1" @@ -6220,14 +5978,9 @@ klaw-sync@^6.0.0: graceful-fs "^4.1.11" klona@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" - integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== - -leaflet-editable@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/leaflet-editable/-/leaflet-editable-1.2.0.tgz#a3a01001764ba58ea923381ee6a1c814708a0b84" - integrity sha512-wG11JwpL8zqIbypTop6xCRGagMuWw68ihYu4uqrqc5Ep0wnEJeyob7NB2Rt5t74Oih4rwJ3OfwaGbzdowOGfYQ== + version "2.0.5" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== leaflet-polylinedecorator@^1.6.0: version "1.6.0" @@ -6236,39 +5989,37 @@ leaflet-polylinedecorator@^1.6.0: dependencies: leaflet-rotatedmarker "^0.2.0" -leaflet-providers@^1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-1.12.0.tgz#bf407f580d9564480e2346bc1e6412ef696624cf" - integrity sha512-pU/mR4B+NbayBGCg5/88dmRq7t1EGiNPhsVGV3yqHuDn594vIwus4CiPVW0RtiKJNKg8Vf1pILAbFl0i+yk+lQ== +leaflet-providers@^1.13.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/leaflet-providers/-/leaflet-providers-1.13.0.tgz#10c843a23d5823a65096d40ad53f27029e13434b" + integrity sha512-f/sN5wdgBbVA2jcCYzScIfYNxKdn2wBJP9bu+5cRX9Xj6g8Bt1G9Sr8WgJAt/ckIFIc3LVVxCBNFpSCfTuUElg== leaflet-rotatedmarker@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz#4467f49f98d1bfd56959bd9c6705203dd2601277" integrity sha1-RGf0n5jRv9VpWb2cZwUgPdJgEnc= -leaflet.gridlayer.googlemutant@0.10.2: - version "0.10.2" - resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.10.2.tgz#3c5351db4230beac1b1ea1f774d9288cfb0b6283" - integrity sha512-r3le0W8izKmF2aeCCYp6P+dLQvPadV/vpJkres0ltDHiWac6qt3fQPNWjQl+8WCsCmcGTb1y5bmHOx0Yj6HA7g== +leaflet.gridlayer.googlemutant@^0.13.4: + version "0.13.4" + resolved "https://registry.yarnpkg.com/leaflet.gridlayer.googlemutant/-/leaflet.gridlayer.googlemutant-0.13.4.tgz#0add37d240c70c999e1f1d341208e6fea2372c40" + integrity sha512-oC6xUSFJ9HP4WIupXakgiYckdBHuHQeSaxTXsVlcvcpfsuYoJ/HFIrz1bmK4Qr/qKO4fY1MDM6AoewU7Bph8ZQ== -leaflet.markercluster@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz#54db42485da32fc3d92c7ae22d0d7982879e0b67" - integrity sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA== +leaflet.markercluster@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056" + integrity sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA== leaflet@^1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== -less-loader@7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-7.3.0.tgz#f9d6d36d18739d642067a05fb5bd70c8c61317e5" - integrity sha512-Mi8915g7NMaLlgi77mgTTQvK022xKRQBIVDSyfl3ErTuBhmZBQab0mjeJjNNqGbdR+qrfTleKXqbGI4uEFavxg== +less-loader@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-10.0.1.tgz#c05aaba68d00400820275f21c2ad87cb9fa9923f" + integrity sha512-Crln//HpW9M5CbtdfWm3IO66Cvx1WhZQvNybXgfB2dD/6Sav9ppw+IWqs/FQKPBFO4B6X0X28Z0WNznshgwUzA== dependencies: klona "^2.0.4" - loader-utils "^2.0.0" - schema-utils "^3.0.0" less@4.1.1: version "4.1.1" @@ -6287,10 +6038,10 @@ less@4.1.1: needle "^2.5.2" source-map "~0.6.0" -license-webpack-plugin@2.3.11: - version "2.3.11" - resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.11.tgz#0d93188a31fce350a44c86212badbaf33dcd29d8" - integrity sha512-0iVGoX5vx0WDy8dmwTTpOOMYiGqILyUbDeVMFH52AjgBlS58lHwOlFMSoqg5nY8Kxl6+FRKyUZY/UdlQaOyqDw== +license-webpack-plugin@2.3.20: + version "2.3.20" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.3.20.tgz#f51fb674ca31519dbedbe1c7aabc036e5a7f2858" + integrity sha512-AHVueg9clOKACSHkhmEI+PCC9x8+qsQVuKECZD3ETxETK5h/PCv5/MUzyG1gm8OMcip/s1tcNxqo9Qb7WhjGsg== dependencies: "@types/webpack-sources" "^0.1.5" webpack-sources "^1.2.0" @@ -6302,17 +6053,22 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +lilconfig@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== + lines-and-columns@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" - integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -loader-runner@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" - integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== +loader-runner@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384" + integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== -loader-utils@2.0.0, loader-utils@^2.0.0: +loader-utils@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== @@ -6321,7 +6077,7 @@ loader-utils@2.0.0, loader-utils@^2.0.0: emojis-list "^3.0.0" json5 "^2.1.2" -loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: +loader-utils@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== @@ -6330,6 +6086,15 @@ loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0: emojis-list "^3.0.0" json5 "^1.0.1" +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -6345,6 +6110,11 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" @@ -6365,19 +6135,20 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@4.17.21, lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@~4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" - integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: - chalk "^4.0.0" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" -log4js@^6.2.1: +log4js@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw== @@ -6389,11 +6160,11 @@ log4js@^6.2.1: streamroller "^2.2.4" loglevel@^1.6.8: - version "1.7.0" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" - integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ== + version "1.8.0" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" + integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -6408,13 +6179,6 @@ lru-cache@^4.1.5: pseudomap "^1.0.2" yallist "^2.1.2" -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -6429,7 +6193,7 @@ magic-string@0.25.7, magic-string@^0.25.0: dependencies: sourcemap-codec "^1.4.4" -make-dir@^2.0.0, make-dir@^2.1.0: +make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== @@ -6449,13 +6213,13 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-fetch-happen@^8.0.9: - version "8.0.14" - resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" - integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== +make-fetch-happen@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== dependencies: agentkeepalive "^4.1.3" - cacache "^15.0.5" + cacache "^15.2.0" http-cache-semantics "^4.1.0" http-proxy-agent "^4.0.1" https-proxy-agent "^5.0.0" @@ -6466,8 +6230,9 @@ make-fetch-happen@^8.0.9: minipass-fetch "^1.3.2" minipass-flush "^1.0.5" minipass-pipeline "^1.2.4" + negotiator "^0.6.2" promise-retry "^2.0.1" - socks-proxy-agent "^5.0.0" + socks-proxy-agent "^6.0.0" ssri "^8.0.0" make-plural@^4.3.0: @@ -6477,6 +6242,13 @@ make-plural@^4.3.0: optionalDependencies: minimist "^1.2.0" +map-age-cleaner@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -6499,15 +6271,6 @@ material-design-icons@^3.0.1: resolved "https://registry.yarnpkg.com/material-design-icons/-/material-design-icons-3.0.1.tgz#9a71c48747218ebca51e51a66da682038cdcb7bf" integrity sha1-mnHEh0chjrylHlGmbaaCA4zct78= -md5.js@^1.3.4: - version "1.3.5" - resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" - integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== - dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - safe-buffer "^5.1.2" - mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -6518,6 +6281,21 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +mem@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/mem/-/mem-8.1.1.tgz#cf118b357c65ab7b7e0817bdf00c8062297c0122" + integrity sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA== + dependencies: + map-age-cleaner "^0.1.3" + mimic-fn "^3.1.0" + +memfs@^3.2.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.0.tgz#8bc12062b973be6b295d4340595736a656f0a257" + integrity sha512-o/RfP0J1d03YwsAxyHxAYs2kyJp55AFkMazlFAZFR2I2IXkxiUTXRabJ6RmNNCQ83LAD2jy52Khj0m3OffpNdA== + dependencies: + fs-monkey "1.0.3" + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -6526,14 +6304,6 @@ memory-fs@^0.4.1: errno "^0.1.3" readable-stream "^2.0.1" -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -6599,73 +6369,58 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" - -miller-rabin@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" - integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== - dependencies: - bn.js "^4.0.0" - brorand "^1.0.1" + picomatch "^2.2.3" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": + version "1.51.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== -"mime-db@>= 1.43.0 < 2": - version "1.45.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" - integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== - -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.34" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== dependencies: - mime-db "1.44.0" + mime-db "1.51.0" mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.4, mime@^2.4.5: - version "2.4.6" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" - integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== +mime@^2.4.4, mime@^2.5.2: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mini-css-extract-plugin@1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.5.tgz#252166e78879c106e0130f229d44e0cbdfcebed3" - integrity sha512-tvmzcwqJJXau4OQE5vT72pRT18o2zF+tQJp8CWchqvfQnTlflkzS+dANYcRdyPRWUWRkfmeNTKltx0NZI/b5dQ== +mimic-fn@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" + integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== + +mini-css-extract-plugin@2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.2.tgz#b3508191ea479388a4715018c99dd3e6dd40d2d2" + integrity sha512-ZmqShkn79D36uerdED+9qdo1ZYG8C1YsWvXu0UMJxurZnSdgz7gQKO2EGv8T55MhDqG3DYmGtizZNpM/UbTlcA== dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - webpack-sources "^1.1.0" + schema-utils "^3.1.0" -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: +minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= - minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -6686,9 +6441,9 @@ minipass-collect@^1.0.2: minipass "^3.0.0" minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" - integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== dependencies: minipass "^3.1.0" minipass-sized "^1.0.3" @@ -6726,9 +6481,9 @@ minipass-sized@^1.0.3: minipass "^3.0.0" minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" - integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + version "3.1.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732" + integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== dependencies: yallist "^4.0.0" @@ -6740,22 +6495,6 @@ minizlib@^2.0.0, minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mississippi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" - integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== - dependencies: - concat-stream "^1.5.0" - duplexify "^3.4.2" - end-of-stream "^1.1.0" - flush-write-stream "^1.0.0" - from2 "^2.1.0" - parallel-transform "^1.1.0" - pump "^3.0.0" - pumpify "^1.3.3" - stream-each "^1.1.0" - through2 "^2.0.0" - mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -6776,17 +6515,10 @@ mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment-timezone@*: - version "0.5.31" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" - integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== - dependencies: - moment ">= 2.9.0" - -moment-timezone@^0.5.33: - version "0.5.33" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.33.tgz#b252fd6bb57f341c9b59a5ab61a8e51a73bbd22c" - integrity sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w== +moment-timezone@*, moment-timezone@^0.5.34: + version "0.5.34" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.34.tgz#a75938f7476b88f155d3504a9343f7519d9a405c" + integrity sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg== dependencies: moment ">= 2.9.0" @@ -6795,28 +6527,11 @@ moment-timezone@^0.5.33: resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== -mousetrap@1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.3.tgz#80fee49665fd478bccf072c9d46bdf1bfed3558a" - integrity sha512-bd+nzwhhs9ifsUrC2tWaSgm24/oo2c83zaRyZQF06hYA6sANfsXHtnZ19AbbbDXCDzeH5nZBSQ4NvCjgD62tJA== - mousetrap@^1.6.0: version "1.6.5" resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== -move-concurrently@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" - integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= - dependencies: - aproba "^1.1.1" - copy-concurrently "^1.0.0" - fs-write-stream-atomic "^1.0.8" - mkdirp "^0.5.1" - rimraf "^2.5.4" - run-queue "^1.0.3" - ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -6827,11 +6542,16 @@ ms@2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.2, ms@^2.0.0, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.0.0, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -6851,14 +6571,14 @@ mute-stream@0.0.8: integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nan@^2.12.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" - integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nanoid@^3.1.22, nanoid@^3.1.23: - version "3.1.23" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" - integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== +nanoid@^3.1.23, nanoid@^3.1.30: + version "3.1.30" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" + integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== nanomatch@^1.2.9: version "1.2.13" @@ -6878,20 +6598,20 @@ nanomatch@^1.2.9: to-regex "^3.0.1" needle@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe" - integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg== + version "2.9.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" + integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ== dependencies: debug "^3.2.6" iconv-lite "^0.4.4" sax "^1.2.4" -negotiator@0.6.2: +negotiator@0.6.2, negotiator@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: +neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -6903,12 +6623,12 @@ ngrx-store-freeze@^0.2.4: dependencies: deep-freeze-strict "^1.1.1" -ngx-clipboard@^14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/ngx-clipboard/-/ngx-clipboard-14.0.1.tgz#fe977ee115f50f6a589487f6f99fa5eef5203763" - integrity sha512-y6fDrvAso1cbM+VvHgB2kJ3dcQ/EBPol33nLaqqKB1jNO/Kd3l17EHdXNW/oKY0wUKCHk7ZCuiinREgUHEYfXg== +ngx-clipboard@^14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/ngx-clipboard/-/ngx-clipboard-14.0.2.tgz#4098b32499a75a41f8c05991f36d30d2a8ec8122" + integrity sha512-zJaFi09D2bq9X1RYvFixE0AfYOI7E8mUO8S0GXcq76HisuE816HRl+8A46G+NML5GPA3Lr5qaGnQJN533So+8g== dependencies: - ngx-window-token ">=4.0.0" + ngx-window-token ">=4.0.0 <6.0.0" tslib "^2.0.0" ngx-color-picker@^11.0.0: @@ -6918,12 +6638,13 @@ ngx-color-picker@^11.0.0: dependencies: tslib "^2.0.0" -ngx-daterangepicker-material@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-4.0.1.tgz#788c2e32eb4717629d4a0e60a60bf8d6430d8c13" - integrity sha512-0gY6DGU+dgYdmoAKrIJSB9xnDqBvj91Yis3II/ZJxxMfZVTG4qMMatck6w8FzdU+CYT64ArCq+Uwa6hJRHX6Nw== +ngx-daterangepicker-material@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ngx-daterangepicker-material/-/ngx-daterangepicker-material-5.0.1.tgz#57403e605b4c317e6f694e3b289732945fcd7e97" + integrity sha512-vbxsiRqyBgXExRBPxuCodLlOOsVEZJ20nTPZks/7z943gZcb2XKwRuQ9hi43MG0xl7RVzDKZdsNKU8bzB86g7g== dependencies: - tslib "^1.10.0" + dayjs "^1.10.4" + tslib "^2.0.0" ngx-drag-drop@^2.0.0: version "2.0.0" @@ -6932,63 +6653,82 @@ ngx-drag-drop@^2.0.0: dependencies: tslib "^1.9.0" -"ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#master": - version "0.0.0" - resolved "git://github.com/thingsboard/ngx-flowchart.git#078bfd2cedeeab412dee922e8066a19be6da7278" +"ngx-flowchart@git://github.com/thingsboard/ngx-flowchart.git#release/1.0.0": + version "0.0.1" + resolved "git://github.com/thingsboard/ngx-flowchart.git#aac96f7e0490a386d58864d7819873e7c63cbb48" dependencies: - tslib "^1.13.0" + tslib "^2.2.0" -ngx-hm-carousel@^2.0.0-rc.1: - version "2.0.0-rc.1" - resolved "https://registry.yarnpkg.com/ngx-hm-carousel/-/ngx-hm-carousel-2.0.0-rc.1.tgz#0ca7663da21a9ac60470b37d300637c29dc64b3a" - integrity sha512-vEMMFctBpQ+0hiwLo97IVmk39He9Gx2Xp6CudD5K1y4xxsuAkpqsGO5jh3KG3i2kMjP8OZY6plTJwIcATvBmLg== +ngx-hm-carousel@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ngx-hm-carousel/-/ngx-hm-carousel-2.0.1.tgz#29574c5128b26d8520b1ef6499eec7efca4e403f" + integrity sha512-iZxe1SVDkmXsSdZlU5nOtYV7hZGiRTEOij7Esnmc+PVgmMmIZe784uZv1RM/cKb3+hNesHjJuYyyNDkD+ryANw== dependencies: + "@types/hammerjs" "^2.0.39" hammerjs "^2.0.8" - resize-observer-polyfill "^1.5.1" + tslib "^2.0.0" -ngx-markdown@^11.1.3: - version "11.1.3" - resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-11.1.3.tgz#4d01c2dc425e5a46d1b6aa8517d9a1c1feaa1efd" - integrity sha512-z32q8l76ubrcP62L03mdvrizwueLBHV10LkT8MEDnFcjmY+8J1PytxFJ9EBTJpvc+CaPolgAoi7felN2XJZTSg== +ngx-markdown@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/ngx-markdown/-/ngx-markdown-12.0.1.tgz#94345e99176533c17396f93e97ff5dc172d8ebcc" + integrity sha512-vMp9SyqmVQZCX374MiCV4sRR1SIv5m3xR2HZ39b3+6/BGjAb46mb4wRXKdIxYUoPba7NYZ8GAt5moUCyVZcCyA== dependencies: "@types/marked" "^2.0.0" - emoji-toolkit "^6.0.1" + emoji-toolkit "^6.5.0" katex "^0.13.0" marked "^2.0.0" prismjs "^1.23.0" - tslib "^2.0.0" + tslib "^2.1.0" -ngx-sharebuttons@^8.0.5: - version "8.0.5" - resolved "https://registry.yarnpkg.com/ngx-sharebuttons/-/ngx-sharebuttons-8.0.5.tgz#49481fcb8bf9541747fd72093eca6f4777c1d371" - integrity sha512-LAaZQk1i/yuYkX+SK72OkJBMNmGJ7SS/yoGEIyuUd7r9F9SwLpWfUAjccmtuM0qDzd48h+1CCnPGIB6WYlKY4g== +ngx-sharebuttons@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/ngx-sharebuttons/-/ngx-sharebuttons-9.0.0.tgz#6919ecb2c77ce6165b443cbc7c44884500186173" + integrity sha512-suoDgXnJwn2EHJzFDvm54KUnW6pGq+APC7VT54HNndwp1lcwXj5QbFgB5Vlk4IfN9VIBOE+IAwdFEdxmIK73Og== dependencies: tslib "^2.0.0" -ngx-translate-messageformat-compiler@^4.9.0: - version "4.9.0" - resolved "https://registry.yarnpkg.com/ngx-translate-messageformat-compiler/-/ngx-translate-messageformat-compiler-4.9.0.tgz#00c3d34f7ed2bed6b406acb2e67b7b8989194430" - integrity sha512-Bat3CjtP7SJ8ks9z3GASW/0wjikf/vE9Vqb0H9SmOgP22CNb22K2oMdPacQQOBjy2hRvcVQ+GDqk5oa7MKiw/A== +ngx-translate-messageformat-compiler@^4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/ngx-translate-messageformat-compiler/-/ngx-translate-messageformat-compiler-4.11.0.tgz#c9b71dd139ba5fcdcd809001e22622de589fd707" + integrity sha512-OdGfWV4fF3DhZqGIHcLmOnQDufugmZ+E90NYr1UPGRZgT10lilr9oLmIrisy3lW4THnZFNo9JXsX7+fX84LbDw== dependencies: tslib "^1.10.0" -ngx-window-token@>=4.0.0: +"ngx-window-token@>=4.0.0 <6.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/ngx-window-token/-/ngx-window-token-5.0.0.tgz#ca63a25038c9fdd73159857276ff67ec7f5b730b" integrity sha512-DhigCrm9QO8R29lqJYzBC9aaTU0KiWgdnt8RNcTN/DvMaS7shfzAqyvUtxSIm/+JR4gW5JKqR/JODU8760nJMw== dependencies: tslib "^2.0.0" +nice-napi@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" + integrity sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA== + dependencies: + node-addon-api "^3.0.0" + node-gyp-build "^4.2.2" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-addon-api@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== +node-gyp-build@^4.2.2: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" + integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== + node-gyp@^7.1.0: version "7.1.2" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" @@ -7005,44 +6745,10 @@ node-gyp@^7.1.0: tar "^6.0.2" which "^2.0.2" -node-libs-browser@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" - integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== - dependencies: - assert "^1.1.1" - browserify-zlib "^0.2.0" - buffer "^4.3.0" - console-browserify "^1.1.0" - constants-browserify "^1.0.0" - crypto-browserify "^3.11.0" - domain-browser "^1.1.1" - events "^3.0.0" - https-browserify "^1.0.0" - os-browserify "^0.3.0" - path-browserify "0.0.1" - process "^0.11.10" - punycode "^1.2.4" - querystring-es3 "^0.2.0" - readable-stream "^2.3.3" - stream-browserify "^2.0.1" - stream-http "^2.7.2" - string_decoder "^1.0.0" - timers-browserify "^2.0.4" - tty-browserify "0.0.0" - url "^0.11.0" - util "^0.11.0" - vm-browserify "^1.0.1" - -node-releases@^1.1.61: - version "1.1.61" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" - integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== - -node-releases@^1.1.71: - version "1.1.72" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe" - integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw== +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== nopt@^5.0.0: version "5.0.0" @@ -7068,10 +6774,10 @@ normalize-range@^0.1.2: resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= -normalize-url@^4.5.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== npm-bundled@^1.1.1: version "1.1.2" @@ -7092,28 +6798,10 @@ npm-normalize-package-bin@^1.0.1: resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -npm-package-arg@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f" - integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig== - dependencies: - hosted-git-info "^3.0.6" - semver "^7.0.0" - validate-npm-package-name "^3.0.0" - -npm-package-arg@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.0.1.tgz#9d76f8d7667b2373ffda60bb801a27ef71e3e270" - integrity sha512-/h5Fm6a/exByzFSTm7jAyHbgOqErl9qSNJDQF32Si/ZzgwT2TERVxRxn3Jurw1wflgyVVAxnFR4fRHPM7y1ClQ== - dependencies: - hosted-git-info "^3.0.2" - semver "^7.0.0" - validate-npm-package-name "^3.0.0" - -npm-package-arg@^8.0.1, npm-package-arg@^8.1.2: - version "8.1.2" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62" - integrity sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA== +npm-package-arg@8.1.5, npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2: + version "8.1.5" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.5.tgz#3369b2d5fe8fdc674baa7f1786514ddc15466e44" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== dependencies: hosted-git-info "^4.0.1" semver "^7.3.4" @@ -7129,16 +6817,7 @@ npm-packlist@^2.1.4: npm-bundled "^1.1.1" npm-normalize-package-bin "^1.0.1" -npm-pick-manifest@6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" - integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== - dependencies: - npm-install-checks "^4.0.0" - npm-package-arg "^8.0.0" - semver "^7.0.0" - -npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: +npm-pick-manifest@6.1.1, npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148" integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== @@ -7148,14 +6827,12 @@ npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: npm-package-arg "^8.1.2" semver "^7.3.4" -npm-registry-fetch@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" - integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== +npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz#68c1bb810c46542760d62a6a965f85a702d43a76" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== dependencies: - "@npmcli/ci-detect" "^1.0.0" - lru-cache "^6.0.0" - make-fetch-happen "^8.0.9" + make-fetch-happen "^9.0.1" minipass "^3.1.3" minipass-fetch "^1.3.0" minipass-json-stream "^1.0.1" @@ -7180,12 +6857,17 @@ npmlog@^4.1.2: set-blocking "~2.0.0" nth-check@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125" - integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q== + version "2.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== dependencies: boolbase "^1.0.0" +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -7196,7 +6878,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@4.x, object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -7210,18 +6892,13 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== - object-is@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" - integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -7235,13 +6912,13 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.0, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== +object.assign@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" has-symbols "^1.0.1" object-keys "^1.1.1" @@ -7288,13 +6965,14 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -open@7.4.0: - version "7.4.0" - resolved "https://registry.yarnpkg.com/open/-/open-7.4.0.tgz#ad95b98f871d9acb0ec8fecc557082cc9986626b" - integrity sha512-PGoBCX/lclIWlpS/R2PQuIR4NJoXh6X5AwVzE7WXnWRGvHg7+4TBCgsujUgiPpm0K1y4qvQeWnCWVTpTKZBtvA== +open@8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/open/-/open-8.2.1.tgz#82de42da0ccbf429bc12d099dad2e0975e14e8af" + integrity sha512-rXILpcQlkF/QuFez2BJDf3GsqpjGKbkUUToAIGo9A0Q6ZkoSGogZJulrUdwRkrAsoQvoZsrjCYt8+zblOk7JQQ== dependencies: - is-docker "^2.0.0" - is-wsl "^2.1.1" + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" open@^7.4.2: version "7.4.2" @@ -7311,17 +6989,18 @@ opn@^5.5.0: dependencies: is-wsl "^1.1.0" -ora@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" - integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g== +ora@5.4.1, ora@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: - bl "^4.0.3" + bl "^4.1.0" chalk "^4.1.0" cli-cursor "^3.1.0" cli-spinners "^2.5.0" is-interactive "^1.0.0" - log-symbols "^4.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" strip-ansi "^6.0.0" wcwidth "^1.0.1" @@ -7332,16 +7011,16 @@ original@^1.0.0: dependencies: url-parse "^1.4.3" -os-browserify@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" - integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= - os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -7354,12 +7033,12 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" - integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-try "^2.0.0" + yocto-queue "^0.1.0" p-locate@^3.0.0: version "3.0.0" @@ -7399,15 +7078,15 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pacote@11.2.4: - version "11.2.4" - resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.2.4.tgz#dc7ca740a573ed86a3bf863511d22c1d413ec82f" - integrity sha512-GfTeVQGJ6WyBQbQD4t3ocHbyOmTQLmWjkCKSZPmKiGFKYKNUaM5U2gbLzUW8WG1XmS9yQFnsTFA0k3o1+q4klQ== +pacote@11.3.5: + version "11.3.5" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-11.3.5.tgz#73cf1fc3772b533f575e39efa96c50be8c3dc9d2" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== dependencies: - "@npmcli/git" "^2.0.1" - "@npmcli/installed-package-contents" "^1.0.5" + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" "@npmcli/promise-spawn" "^1.2.0" - "@npmcli/run-script" "^1.3.0" + "@npmcli/run-script" "^1.8.2" cacache "^15.0.5" chownr "^2.0.0" fs-minipass "^2.1.0" @@ -7417,27 +7096,18 @@ pacote@11.2.4: npm-package-arg "^8.0.1" npm-packlist "^2.1.4" npm-pick-manifest "^6.0.0" - npm-registry-fetch "^9.0.0" - promise-retry "^1.1.1" - read-package-json-fast "^1.1.3" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" rimraf "^3.0.2" - ssri "^8.0.0" + ssri "^8.0.1" tar "^6.1.0" -pako@~1.0.2, pako@~1.0.5: +pako@^1.0.3, pako@~1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== -parallel-transform@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" - integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== - dependencies: - cyclist "^1.0.1" - inherits "^2.0.3" - readable-stream "^2.1.5" - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -7445,17 +7115,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.5: - version "5.1.6" - resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" - integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== - dependencies: - asn1.js "^5.2.0" - browserify-aes "^1.0.0" - evp_bytestokey "^1.0.0" - pbkdf2 "^3.0.3" - safe-buffer "^5.1.1" - parse-json@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -7532,11 +7191,6 @@ patch-package@^6.4.7: slash "^2.0.0" tmp "^0.0.33" -path-browserify@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" - integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== - path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" @@ -7568,9 +7222,9 @@ path-key@^2.0.0, path-key@^2.0.1: integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-to-regexp@0.1.7: version "0.1.7" @@ -7582,26 +7236,25 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.1.tgz#cb8724b0fada984596856d1a6ebafd3584654b94" - integrity sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg== - dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== pify@^2.0.0, pify@^2.3.0: version "2.3.0" @@ -7625,6 +7278,17 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +piscina@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/piscina/-/piscina-3.1.0.tgz#2333636865b6cb69c5a370bbc499a98cabcf3e04" + integrity sha512-KTW4sjsCD34MHrUbx9eAAbuUSpVj407hQSgk/6Epkg0pbRBmv4a3UX7Sr8wxm9xYqQLnsN4mFOjqGDzHAdgKQg== + dependencies: + eventemitter-asyncresource "^1.0.0" + hdr-histogram-js "^2.0.1" + hdr-histogram-percentiles-obj "^3.0.0" + optionalDependencies: + nice-napi "^1.0.2" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -7639,17 +7303,17 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" -pngjs@^3.3.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" - integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== -pnp-webpack-plugin@1.6.4: - version "1.6.4" - resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz#c9711ac4dc48a685dabafc86f8b6dd9f8df84149" - integrity sha512-7Wjy+9E3WwLOEL30D+m8TSTF7qJJUJLONBnwQp0518siuMxUQUbgZwssaFX+QKlZkjHZcw/IpZCt/H0srrntSg== +polygon-clipping@0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/polygon-clipping/-/polygon-clipping-0.15.3.tgz#0215840438470ba2e9e6593625e4ea5c1087b4b7" + integrity sha512-ho0Xx5DLkgxRx/+n4O74XyJ67DcyN3Tu9bGYKsnTukGAW6ssnuak6Mwcyb1wHy9MZc9xsUWqIoiazkZB5weECg== dependencies: - ts-pnp "^1.1.6" + splaytree "^3.1.0" popper.js@1.16.1-lts: version "1.16.1-lts" @@ -7670,6 +7334,14 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= +postcss-attribute-case-insensitive@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.2.tgz#d93e46b504589e94ac7277b0463226c68041a880" + integrity sha512-clkFxk/9pcdb4Vkn0hAHq3YnxBQ2p0CGD1dy24jN+reBck+EWxMbxSUqN4Yj7t0w8csl87K6p0gxBe1utkJsYA== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^6.0.2" + postcss-calc@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" @@ -7678,22 +7350,96 @@ postcss-calc@^8.0.0: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.2" -postcss-colormin@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.1.1.tgz#834d262f6021f832d9085e355f08ade288a92a1d" - integrity sha512-SyTmqKKN6PyYNeeKEC0hqIP5CDuprO1hHurdW1aezDyfofDUOn7y7MaxcolbsW3oazPwFiGiY30XRiW1V4iZpA== +postcss-color-functional-notation@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0" + integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g== dependencies: - browserslist "^4.16.0" - colord "^2.0.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-gray@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547" + integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-color-hex-alpha@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.3.tgz#a8d9ca4c39d497c9661e374b9c51899ef0f87388" + integrity sha512-PF4GDel8q3kkreVXKLAGNpHKilXsZ6xuu+mOQMHWHLPNyjiUBOr75sp5ZKJfmv1MCus5/DWUGcK9hm6qHEnXYw== + dependencies: + postcss "^7.0.14" + postcss-values-parser "^2.0.1" + +postcss-color-mod-function@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d" + integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-color-rebeccapurple@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77" + integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-colormin@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d" + integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.9.1" postcss-value-parser "^4.1.0" -postcss-convert-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232" - integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg== +postcss-convert-values@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059" + integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg== dependencies: postcss-value-parser "^4.1.0" +postcss-custom-media@^7.0.8: + version "7.0.8" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.8.tgz#fffd13ffeffad73621be5f387076a28b00294e0c" + integrity sha512-c9s5iX0Ge15o00HKbuRuTqNndsJUbaXdiNsksnVH8H4gdc+zbLzr/UasOwNG6CTDpLFekVY4672eWdiiWu2GUg== + dependencies: + postcss "^7.0.14" + +postcss-custom-properties@^8.0.11: + version "8.0.11" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.11.tgz#2d61772d6e92f22f5e0d52602df8fae46fa30d97" + integrity sha512-nm+o0eLdYqdnJ5abAJeXp4CEU1c1k+eB2yMCvhgzsds/e0umabFrN6HoTy/8Q4K5ilxERdl/JD1LO5ANoYBeMA== + dependencies: + postcss "^7.0.17" + postcss-values-parser "^2.0.1" + +postcss-custom-selectors@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba" + integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-dir-pseudo-class@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2" + integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + postcss-discard-comments@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe" @@ -7714,45 +7460,123 @@ postcss-discard-overridden@^5.0.1: resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== -postcss-import@14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.0.0.tgz#3ed1dadac5a16650bde3f4cdea6633b9c3c78296" - integrity sha512-gFDDzXhqr9ELmnLHgCC3TbGfA6Dm/YMb/UN8/f7Uuq4fL7VTk2vOIj6hwINEwbokEmp123bLD7a5m+E+KIetRg== +postcss-double-position-gradients@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e" + integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA== + dependencies: + postcss "^7.0.5" + postcss-values-parser "^2.0.0" + +postcss-env-function@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7" + integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-focus-visible@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e" + integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g== + dependencies: + postcss "^7.0.2" + +postcss-focus-within@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680" + integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w== + dependencies: + postcss "^7.0.2" + +postcss-font-variant@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.1.tgz#42d4c0ab30894f60f98b17561eb5c0321f502641" + integrity sha512-I3ADQSTNtLTTd8uxZhtSOrTCQ9G4qUVKPjHiDk0bV75QSxXjVWiJVJ2VLdspGUi9fbW9BcjKJoRvxAH1pckqmA== + dependencies: + postcss "^7.0.2" + +postcss-gap-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715" + integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg== + dependencies: + postcss "^7.0.2" + +postcss-image-set-function@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288" + integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-import@14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.0.2.tgz#60eff77e6be92e7b67fe469ec797d9424cae1aa1" + integrity sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g== dependencies: postcss-value-parser "^4.0.0" read-cache "^1.0.0" resolve "^1.1.7" -postcss-loader@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-4.2.0.tgz#f6993ea3e0f46600fb3ee49bbd010448123a7db4" - integrity sha512-mqgScxHqbiz1yxbnNcPdKYo/6aVt+XExURmEbQlviFVWogDbM4AJ0A/B+ZBpYsJrTRxKw7HyRazg9x0Q9SWwLA== +postcss-initial@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.4.tgz#9d32069a10531fe2ecafa0b6ac750ee0bc7efc53" + integrity sha512-3RLn6DIpMsK1l5UUy9jxQvoDeUN4gP939tDcKUHD/kM8SGSKbFAnvkpFpj3Bhtz3HGk1jWY5ZNWX6mPta5M9fg== + dependencies: + postcss "^7.0.2" + +postcss-lab-function@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e" + integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg== + dependencies: + "@csstools/convert-colors" "^1.4.0" + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-loader@6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.1.1.tgz#58dd0a3accd9bc87cc52eff75244db578d11301a" + integrity sha512-lBmJMvRh1D40dqpWKr9Rpygwxn8M74U9uaCSeYGNKLGInbk9mXBt1ultHf2dH9Ghk6Ue4UXlXWwGMH9QdUJ5ug== + dependencies: + cosmiconfig "^7.0.0" + klona "^2.0.4" + semver "^7.3.5" + +postcss-logical@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5" + integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA== + dependencies: + postcss "^7.0.2" + +postcss-media-minmax@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5" + integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw== dependencies: - cosmiconfig "^7.0.0" - klona "^2.0.4" - loader-utils "^2.0.0" - schema-utils "^3.0.0" - semver "^7.3.4" + postcss "^7.0.2" -postcss-merge-longhand@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41" - integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw== +postcss-merge-longhand@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz#41f4f3270282ea1a145ece078b7679f0cef21c32" + integrity sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw== dependencies: - css-color-names "^1.0.1" postcss-value-parser "^4.1.0" stylehacks "^5.0.1" -postcss-merge-rules@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.1.tgz#4ff61c5089d86845184a0f149e88d687028bef7e" - integrity sha512-UR6R5Ph0c96QB9TMBH3ml8/kvPCThPHepdhRqAbvMRDRHQACPC8iM5NpfIC03+VRMZTGXy4L/BvFzcDFCgb+fA== +postcss-merge-rules@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.3.tgz#b5cae31f53129812a77e3eb1eeee448f8cf1a1db" + integrity sha512-cEKTMEbWazVa5NXd8deLdCnXl+6cYG7m2am+1HzqH0EnTdy8fRysatkaXb2dEnR+fdaDxTvuZ5zoBdv6efF6hg== dependencies: - browserslist "^4.16.0" + browserslist "^4.16.6" caniuse-api "^3.0.0" cssnano-utils "^2.0.1" postcss-selector-parser "^6.0.5" - vendors "^1.0.3" postcss-minify-font-values@^5.0.1: version "5.0.1" @@ -7761,25 +7585,24 @@ postcss-minify-font-values@^5.0.1: dependencies: postcss-value-parser "^4.1.0" -postcss-minify-gradients@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.1.tgz#2dc79fd1a1afcb72a9e727bc549ce860f93565d2" - integrity sha512-odOwBFAIn2wIv+XYRpoN2hUV3pPQlgbJ10XeXPq8UY2N+9ZG42xu45lTn/g9zZ+d70NKSQD6EOi6UiCMu3FN7g== +postcss-minify-gradients@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e" + integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q== dependencies: + colord "^2.9.1" cssnano-utils "^2.0.1" - is-color-stop "^1.1.0" postcss-value-parser "^4.1.0" -postcss-minify-params@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c" - integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw== +postcss-minify-params@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.2.tgz#1b644da903473fbbb18fbe07b8e239883684b85c" + integrity sha512-qJAPuBzxO1yhLad7h2Dzk/F7n1vPyfHfCCh5grjGfjhi1ttCnq4ZXGIW77GSrEbh9Hus9Lc/e/+tB4vh3/GpDg== dependencies: alphanum-sort "^1.0.2" - browserslist "^4.16.0" + browserslist "^4.16.6" cssnano-utils "^2.0.1" postcss-value-parser "^4.1.0" - uniqs "^2.0.0" postcss-minify-selectors@^5.1.0: version "5.1.0" @@ -7817,6 +7640,13 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-nesting@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.1.tgz#b50ad7b7f0173e5b5e3880c3501344703e04c052" + integrity sha512-FrorPb0H3nuVq0Sff7W2rnc3SmIcruVC6YwpcS+k687VxyxO33iE1amna7wHuRVzM8vfiYofXSBHNAZ3QhLvYg== + dependencies: + postcss "^7.0.2" + postcss-normalize-charset@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" @@ -7868,13 +7698,13 @@ postcss-normalize-unicode@^5.0.1: browserslist "^4.16.0" postcss-value-parser "^4.1.0" -postcss-normalize-url@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.1.tgz#ffa9fe545935d8b57becbbb7934dd5e245513183" - integrity sha512-hkbG0j58Z1M830/CJ73VsP7gvlG1yF+4y7Fd1w4tD2c7CaA2Psll+pQ6eQhth9y9EaqZSLzamff/D0MZBMbYSg== +postcss-normalize-url@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.3.tgz#42eca6ede57fe69075fab0f88ac8e48916ef931c" + integrity sha512-qWiUMbvkRx3kc1Dp5opzUwc7MBWZcSDK2yofCmdvFBCpx+zFPkxBC1FASQ59Pt+flYfj/nTZSkmF56+XG5elSg== dependencies: is-absolute-url "^3.0.3" - normalize-url "^4.5.0" + normalize-url "^6.0.1" postcss-value-parser "^4.1.0" postcss-normalize-whitespace@^5.0.1: @@ -7884,20 +7714,93 @@ postcss-normalize-whitespace@^5.0.1: dependencies: postcss-value-parser "^4.1.0" -postcss-ordered-values@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.1.tgz#79ef6e2bd267ccad3fc0c4f4a586dfd01c131f64" - integrity sha512-6mkCF5BQ25HvEcDfrMHCLLFHlraBSlOXFnQMHYhSpDO/5jSR1k8LdEXOkv+7+uzW6o6tBYea1Km0wQSRkPJkwA== +postcss-ordered-values@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044" + integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ== dependencies: cssnano-utils "^2.0.1" postcss-value-parser "^4.1.0" -postcss-reduce-initial@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" - integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== +postcss-overflow-shorthand@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30" + integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g== dependencies: - browserslist "^4.16.0" + postcss "^7.0.2" + +postcss-page-break@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf" + integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ== + dependencies: + postcss "^7.0.2" + +postcss-place@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62" + integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg== + dependencies: + postcss "^7.0.2" + postcss-values-parser "^2.0.0" + +postcss-preset-env@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.7.0.tgz#c34ddacf8f902383b35ad1e030f178f4cdf118a5" + integrity sha512-eU4/K5xzSFwUFJ8hTdTQzo2RBLbDVt83QZrAvI07TULOkmyQlnYlpwep+2yIK+K+0KlZO4BvFcleOCCcUtwchg== + dependencies: + autoprefixer "^9.6.1" + browserslist "^4.6.4" + caniuse-lite "^1.0.30000981" + css-blank-pseudo "^0.1.4" + css-has-pseudo "^0.10.0" + css-prefers-color-scheme "^3.1.1" + cssdb "^4.4.0" + postcss "^7.0.17" + postcss-attribute-case-insensitive "^4.0.1" + postcss-color-functional-notation "^2.0.1" + postcss-color-gray "^5.0.0" + postcss-color-hex-alpha "^5.0.3" + postcss-color-mod-function "^3.0.3" + postcss-color-rebeccapurple "^4.0.1" + postcss-custom-media "^7.0.8" + postcss-custom-properties "^8.0.11" + postcss-custom-selectors "^5.1.2" + postcss-dir-pseudo-class "^5.0.0" + postcss-double-position-gradients "^1.0.0" + postcss-env-function "^2.0.2" + postcss-focus-visible "^4.0.0" + postcss-focus-within "^3.0.0" + postcss-font-variant "^4.0.0" + postcss-gap-properties "^2.0.0" + postcss-image-set-function "^3.0.1" + postcss-initial "^3.0.0" + postcss-lab-function "^2.0.1" + postcss-logical "^3.0.0" + postcss-media-minmax "^4.0.0" + postcss-nesting "^7.0.0" + postcss-overflow-shorthand "^2.0.0" + postcss-page-break "^2.0.0" + postcss-place "^4.0.1" + postcss-pseudo-class-any-link "^6.0.0" + postcss-replace-overflow-wrap "^3.0.0" + postcss-selector-matches "^4.0.0" + postcss-selector-not "^4.0.0" + +postcss-pseudo-class-any-link@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1" + integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew== + dependencies: + postcss "^7.0.2" + postcss-selector-parser "^5.0.0-rc.3" + +postcss-reduce-initial@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.2.tgz#fa424ce8aa88a89bc0b6d0f94871b24abe94c048" + integrity sha512-v/kbAAQ+S1V5v9TJvbGkV98V2ERPdU6XvMcKMjqAlYiJ2NtsHGlKYLPjWWcXlaTKNxooId7BGxeraK8qXvzKtw== + dependencies: + browserslist "^4.16.6" caniuse-api "^3.0.0" postcss-reduce-transforms@^5.0.1: @@ -7908,17 +7811,39 @@ postcss-reduce-transforms@^5.0.1: cssnano-utils "^2.0.1" postcss-value-parser "^4.1.0" -postcss-selector-parser@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz#56075a1380a04604c38b063ea7767a129af5c2b3" - integrity sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw== +postcss-replace-overflow-wrap@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c" + integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw== dependencies: - cssesc "^3.0.0" + postcss "^7.0.2" + +postcss-selector-matches@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff" + integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-not@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.1.tgz#263016eef1cf219e0ade9a913780fc1f48204cbf" + integrity sha512-YolvBgInEK5/79C+bdFMyzqTg6pkYqDbzZIST/PDMqa/o3qtXenD05apBG2jLgT0/BQ77d4U2UK12jWpilqMAQ== + dependencies: + balanced-match "^1.0.0" + postcss "^7.0.2" + +postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c" + integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ== + dependencies: + cssesc "^2.0.0" indexes-of "^1.0.1" uniq "^1.0.1" - util-deprecate "^1.0.2" -postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: version "6.0.6" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== @@ -7926,100 +7851,92 @@ postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss-svgo@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.1.tgz#6ed5e01e164e59204978994d844c653a331a8100" - integrity sha512-cD7DFo6tF9i5eWvwtI4irKOHCpmASFS0xvZ5EQIgEdA1AWfM/XiHHY/iss0gcKHhkqwgYmuo2M0KhJLd5Us6mg== +postcss-svgo@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30" + integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA== dependencies: postcss-value-parser "^4.1.0" - svgo "^2.3.0" + svgo "^2.7.0" -postcss-unique-selectors@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc" - integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w== +postcss-unique-selectors@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz#5d6893daf534ae52626708e0d62250890108c0c1" + integrity sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA== dependencies: alphanum-sort "^1.0.2" postcss-selector-parser "^6.0.5" - uniqs "^2.0.0" postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" - integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f" + integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg== + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" -postcss@8.2.14: - version "8.2.14" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.14.tgz#dcf313eb8247b3ce8078d048c0e8262ca565ad2b" - integrity sha512-+jD0ZijcvyCqPQo/m/CW0UcARpdFylq04of+Q7RKX6f/Tu+dvpUI/9Sp81+i6/vJThnOBX09Quw0ZLOVwpzX3w== +postcss@8.3.6: + version "8.3.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" + integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== dependencies: colorette "^1.2.2" - nanoid "^3.1.22" - source-map "^0.6.1" + nanoid "^3.1.23" + source-map-js "^0.6.2" -postcss@^7.0.35: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== +postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.32, postcss@^7.0.35, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== dependencies: - chalk "^2.4.2" + picocolors "^0.2.1" source-map "^0.6.1" - supports-color "^6.1.0" -postcss@^8.1.4: - version "8.3.0" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f" - integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ== +postcss@^8.2.15, postcss@^8.3.5, postcss@^8.3.7: + version "8.4.4" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.4.tgz#d53d4ec6a75fd62557a66bb41978bf47ff0c2869" + integrity sha512-joU6fBsN6EIer28Lj6GDFoC/5yOZzLCfn0zHAn/MYXI7aPt4m4hK5KC5ovEZXy+lnCjmYIbQWngvju2ddyEr8Q== dependencies: - colorette "^1.2.2" - nanoid "^3.1.23" - source-map-js "^0.6.2" + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" postinstall-prepare@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postinstall-prepare/-/postinstall-prepare-2.0.0.tgz#2a6867c1a13a05502aa115d0495efbbd778769cb" integrity sha512-lLFwEKdnGLAaRAm8OpXP6HwrXRW+b8Hh9vRhVHZKmCdobd+D21YM38BCDsi3zrePLSe8Tt0H/mbYkh7/ySQQMg== -prettier@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" - integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== +prettier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893" + integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg== pretty-bytes@^5.3.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== -prismjs@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" - integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== - optionalDependencies: - clipboard "^2.0.0" +prismjs@^1.23.0, prismjs@^1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise-retry@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" - integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= - dependencies: - err-code "^1.0.0" - retry "^0.10.0" - promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -8028,7 +7945,7 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8064,11 +7981,11 @@ protractor@~7.0.0: yargs "^15.3.1" proxy-addr@~2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" + forwarded "0.2.0" ipaddr.js "1.9.1" prr@~1.0.1: @@ -8086,26 +8003,6 @@ psl@^1.1.28: resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -public-encrypt@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" - integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== - dependencies: - bn.js "^4.1.0" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - parse-asn1 "^5.0.0" - randombytes "^2.0.1" - safe-buffer "^5.1.2" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" @@ -8114,25 +8011,11 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -8153,18 +8036,15 @@ qjobs@^1.2.0: resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== -qrcode@^1.4.4: - version "1.4.4" - resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83" - integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q== +qrcode@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.0.tgz#95abb8a91fdafd86f8190f2836abbfc500c72d1b" + integrity sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ== dependencies: - buffer "^5.4.3" - buffer-alloc "^1.2.0" - buffer-from "^1.1.1" dijkstrajs "^1.0.1" - isarray "^2.0.1" - pngjs "^3.3.0" - yargs "^13.2.4" + encode-utf8 "^1.0.3" + pngjs "^5.0.0" + yargs "^15.3.1" qs@6.7.0: version "6.7.0" @@ -8176,11 +8056,6 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== -querystring-es3@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" - integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= - querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -8191,28 +8066,23 @@ querystringify@^2.1.1: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== -raf@^3.4.0, raf@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: +quickselect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" + integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== + +randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" -randomfill@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" - integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== - dependencies: - randombytes "^2.0.5" - safe-buffer "^5.1.0" - range-parser@^1.2.1, range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -8235,7 +8105,7 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -raw-loader@4.0.2: +raw-loader@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== @@ -8243,127 +8113,123 @@ raw-loader@4.0.2: loader-utils "^2.0.0" schema-utils "^3.0.0" +rbush@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" + integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== + dependencies: + quickselect "^2.0.0" + rc-align@^4.0.0: - version "4.0.8" - resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.8.tgz#276c3f5dfadf0de4bb95392cb81568c9e947a668" - integrity sha512-2sRUkmB8z4UEXzaS+lDHzXMoR8HrtKH9nn2yHlHVNyUTnaucjMFbdEoCk+hO1g7cpIgW0MphG8i0EH2scSesfw== + version "4.0.11" + resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.11.tgz#8198c62db266bc1b8ef05e56c13275bf72628a5e" + integrity sha512-n9mQfIYQbbNTbefyQnRHZPWuTEwG1rY4a9yKlIWHSTbgwI+XUMGRYd0uJ5pE2UbrNX0WvnMBA1zJ3Lrecpra/A== dependencies: "@babel/runtime" "^7.10.1" classnames "2.x" dom-align "^1.7.0" + lodash "^4.17.21" rc-util "^5.3.0" resize-observer-polyfill "^1.5.1" -rc-animate@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/rc-animate/-/rc-animate-3.1.1.tgz#defdd863f56816c222534e4dc68feddecd081386" - integrity sha512-8wg2Zg3EETy0k/9kYuis30NJNQg1D6/WSQwnCiz6SvyxQXNet/rVraRz3bPngwY6rcU2nlRvoShiYOorXyF7Sg== +rc-motion@^2.0.0, rc-motion@^2.0.1: + version "2.4.4" + resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-2.4.4.tgz#e995d5fa24fc93065c24f714857cf2677d655bb0" + integrity sha512-ms7n1+/TZQBS0Ydd2Q5P4+wJTSOrhIrwNxLXCZpR7Fa3/oac7Yi803HDALc2hLAKaCTQtw9LmQeB58zcwOsqlQ== dependencies: - "@ant-design/css-animation" "^1.7.2" - classnames "^2.2.6" - raf "^3.4.0" - rc-util "^4.15.3" + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-util "^5.2.1" -rc-motion@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rc-motion/-/rc-motion-1.1.2.tgz#07212f1b96c715b8245ec121339146c4a9b0968c" - integrity sha512-YC/E7SSWKBFakYg4PENhSRWD4ZLDqkI7FKmutJcrMewZ91/ZIWfoZSDvPaBdKO0hsFrrzWepFhXQIq0FNnCMWA== +rc-overflow@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.2.2.tgz#95b0222016c0cdbdc0db85f569c262e7706a5f22" + integrity sha512-X5kj9LDU1ue5wHkqvCprJWLKC+ZLs3p4He/oxjZ1Q4NKaqKBaYf5OdSzRSgh3WH8kSdrfU8LjvlbWnHgJOEkNQ== dependencies: "@babel/runtime" "^7.11.1" classnames "^2.2.1" - raf "^3.4.1" - rc-util "^5.0.6" + rc-resize-observer "^1.0.0" + rc-util "^5.5.1" -rc-select@~10.5.1: - version "10.5.1" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-10.5.1.tgz#4d4c5d4f8d2fd3b7e3dccf74e4c43142b2979247" - integrity sha512-fZraoNNhjUmDJccfk6VYgrgHBWFmHWh/NJZh2Xttcm/usOolYI1RjO9ikP4QGhzlJBFtnTLH2pE2nchjn3TCXA== +rc-resize-observer@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.1.1.tgz#ef666e38065f550730176404bae2ce8ca5fb1ac4" + integrity sha512-5A3B9ha297ItltzXl812WFE36SyRDTNclfrXE3FL1pEwXkBh7iSEzxjzfwsPeMcF9ahy3ZoxLgLuRksXBGGD6A== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + rc-util "^5.15.0" + resize-observer-polyfill "^1.5.1" + +rc-select@~13.2.0: + version "13.2.1" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-13.2.1.tgz#d69675f8bc72622a8f3bc024fa21bfee8d56257d" + integrity sha512-L2cJFAjVEeDiNVa/dlOVKE79OUb0J7sUBvWN3Viav3XHcjvv9Ovn4D8J9QhBSlDXeGuczZ81CZI3BbdHD25+Gg== dependencies: "@babel/runtime" "^7.10.1" classnames "2.x" - rc-animate "^3.0.0" - rc-trigger "^4.3.0" - rc-util "^5.0.1" - rc-virtual-list "^1.1.2" - warning "^4.0.3" + rc-motion "^2.0.1" + rc-overflow "^1.0.0" + rc-trigger "^5.0.4" + rc-util "^5.9.8" + rc-virtual-list "^3.2.0" -rc-trigger@^4.3.0: - version "4.4.3" - resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-4.4.3.tgz#ed449cd6cce446555bc57f4394229c5c7154f4b0" - integrity sha512-yq/WyuiPwxd2q6jy+VPyy0GUCRFJ2eFqAaCwPE27AOftXeIupOcJ/2t1wakSq63cfk7qtzev5DKHUAjb8LOJCw== +rc-trigger@^5.0.4: + version "5.2.10" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.10.tgz#8a0057a940b1b9027eaa33beec8a6ecd85cce2b1" + integrity sha512-FkUf4H9BOFDaIwu42fvRycXMAvkttph9AlbCZXssZDVzz2L+QZ0ERvfB/4nX3ZFPh1Zd+uVGr1DEDeXxq4J1TA== dependencies: "@babel/runtime" "^7.11.2" classnames "^2.2.6" - raf "^3.4.1" rc-align "^4.0.0" - rc-motion "^1.0.0" - rc-util "^5.0.1" - -rc-util@^4.15.3: - version "4.21.1" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.21.1.tgz#88602d0c3185020aa1053d9a1e70eac161becb05" - integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg== - dependencies: - add-dom-event-listener "^1.1.0" - prop-types "^15.5.10" - react-is "^16.12.0" - react-lifecycles-compat "^3.0.4" - shallowequal "^1.1.0" - -rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.3.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.4.0.tgz#688eaeecfdae9dae2bfdf10bedbe884591dba004" - integrity sha512-kXDn1JyLJTAWLBFt+fjkTcUtXhxKkipQCobQmxIEVrX62iXgo24z8YKoWehWfMxPZFPE+RXqrmEu9j5kHz/Lrg== - dependencies: - react-is "^16.12.0" - shallowequal "^1.1.0" + rc-motion "^2.0.0" + rc-util "^5.5.0" -rc-util@^5.0.6: - version "5.7.0" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.7.0.tgz#776b14cf5bbfc24f419fd40c42ffadddda0718fc" - integrity sha512-0hh5XkJ+vBDeMJsHElqT1ijMx+gC3gpClwQ10h/5hccrrgrMx8VUem183KLlH1YrWCfMMPmDXWWNnwsn+p6URw== +rc-util@^5.0.7, rc-util@^5.15.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0, rc-util@^5.5.1, rc-util@^5.9.8: + version "5.16.1" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.16.1.tgz#374db7cb735512f05165ddc3d6b2c61c21b8b4e3" + integrity sha512-kSCyytvdb3aRxQacS/71ta6c+kBWvM1v8/2h9d/HaNWauc3qB8pLnF20PJ8NajkNN8gb+rR1l0eWO+D4Pz+LLQ== dependencies: "@babel/runtime" "^7.12.5" react-is "^16.12.0" shallowequal "^1.1.0" -rc-virtual-list@^1.1.2: - version "1.1.6" - resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-1.1.6.tgz#b255baf9aacde149a8893324e6307214094f4c0a" - integrity sha512-u3+izqWL8p8bQy8nYH48qWpiGyxR/ye8D2k0zJlXmfYeL55/xh83YrzHqiDzO78uj0Ewag3nXDA0JTVrYO7ygQ== +rc-virtual-list@^3.2.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.4.2.tgz#1078327aa7230b5e456d679ed2ce99f3c036ebd1" + integrity sha512-OyVrrPvvFcHvV0ssz5EDZ+7Rf5qLat/+mmujjchNw5FfbJWNDwkpQ99EcVE6+FtNRmX9wFa1LGNpZLUTvp/4GQ== dependencies: classnames "^2.2.6" - raf "^3.4.1" - rc-util "^5.0.0" + rc-resize-observer "^1.0.0" + rc-util "^5.0.7" -react-ace@^9.1.4: - version "9.1.4" - resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.1.4.tgz#7c45c361aa5fe1efa3313fa876bce30aa64a244f" - integrity sha512-4DBWvElbVR3WhsA2HhQ524K9Yoa/J/sjuBV9NUZ+yar3Q4BGJRTnhY6pM0INffH1IkBZHKIOyz34XHjc7RNTpw== +react-ace@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-9.5.0.tgz#b6c32b70d404dd821a7e01accc2d76da667ff1f7" + integrity sha512-4l5FgwGh6K7A0yWVMQlPIXDItM4Q9zzXRqOae8KkCl6MkOob7sC1CzHxZdOGvV+QioKWbX2p5HcdOVUv6cAdSg== dependencies: - ace-builds "^1.4.6" - diff-match-patch "^1.0.4" + ace-builds "^1.4.13" + diff-match-patch "^1.0.5" lodash.get "^4.4.2" lodash.isequal "^4.5.0" prop-types "^15.7.2" -react-dom@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" - integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== +react-dom@^17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" + integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" + scheduler "^0.20.2" -react-dropzone@^11.2.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.2.0.tgz#4e54fa3479e6b6bb93f67914e4a27f1260807fdb" - integrity sha512-S/qaXQHCCg7MVlcrhqd05MLC6DupITLUB0CFn3iCLs6OTjzxdGDF1WTktTe5Jyq8jZdxYfMHNUZOHL0mg+K0Dw== +react-dropzone@^11.4.2: + version "11.4.2" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.4.2.tgz#1eb99e9def4cc7520f4f58e85c853ce52c483d56" + integrity sha512-ocYzYn7Qgp0tFc1gQtUTOaHHSzVTwhWHxxY+r7cj2jJTPfMTZB5GWSJHdIVoxsl+EQENpjJ/6Zvcw0BqKZQ+Eg== dependencies: - attr-accept "^2.0.0" - file-selector "^0.1.12" + attr-accept "^2.2.1" + file-selector "^0.2.2" prop-types "^15.7.2" react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1: @@ -8376,29 +8242,23 @@ react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - react-transition-group@^4.0.0, react-transition-group@^4.4.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" - integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== dependencies: "@babel/runtime" "^7.5.5" dom-helpers "^5.0.1" loose-envify "^1.4.0" prop-types "^15.6.2" -react@~16.14.0: - version "16.14.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== +react@~17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" + integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.2" reactcss@^1.2.3: version "1.2.3" @@ -8414,23 +8274,15 @@ read-cache@^1.0.0: dependencies: pify "^2.3.0" -read-package-json-fast@^1.1.3: - version "1.2.2" - resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-1.2.2.tgz#fba77b0b0d66b1ab344e214cb0876577e749c423" - integrity sha512-39DbPJjkltEzfXJXB6D8/Ir3GFOU2YbSKa2HaB/Y3nKrc/zY+0XrALpID6/13ezWyzqvOHrBbR4t4cjQuTdBVQ== - dependencies: - json-parse-even-better-errors "^2.3.0" - npm-normalize-package-bin "^1.0.1" - read-package-json-fast@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.2.tgz#2dcb24d9e8dd50fb322042c8c35a954e6cc7ac9e" - integrity sha512-5fyFUyO9B799foVk4n6ylcoAktG/FbE3jwRKxvwaeSrIunaoMc0u81dzXxjeAFKOce7O5KncdfwpGvvs6r5PsQ== + version "2.0.3" + resolved "https://registry.yarnpkg.com/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz#323ca529630da82cb34b36cc0b996693c98c2b83" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== dependencies: json-parse-even-better-errors "^2.3.0" npm-normalize-package-bin "^1.0.1" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@~2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -8443,7 +8295,7 @@ read-package-json-fast@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -8461,17 +8313,10 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" -readdirp@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" - integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== - dependencies: - picomatch "^2.2.1" - -readdirp@~3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" - integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== dependencies: picomatch "^2.2.1" @@ -8480,22 +8325,22 @@ reflect-metadata@^0.1.2: resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== -regenerate-unicode-properties@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" - integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA== +regenerate-unicode-properties@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" + integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== dependencies: - regenerate "^1.4.0" + regenerate "^1.4.2" -regenerate@^1.4.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.1.tgz#cad92ad8e6b591773485fbe05a485caf4f457e6f" - integrity sha512-j2+C8+NtXQgEKWk49MMP5P/u2GhnahTtVkRIHr5R5lVRlbKvmQ+oS+A5aLKWp2ma5VkT8sh6v+v4hbH0YHR66A== +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@0.13.7, regenerator-runtime@^0.13.4: - version "0.13.7" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" - integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== +regenerator-runtime@0.13.9, regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== regenerator-transform@^0.14.2: version "0.14.5" @@ -8518,34 +8363,34 @@ regex-parser@^2.2.11: integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== regexp.prototype.flags@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" -regexpu-core@^4.7.0, regexpu-core@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.1.tgz#2dea5a9a07233298fbf0db91fa9abc4c6e0f8ad6" - integrity sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ== +regexpu-core@^4.7.1: + version "4.8.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0" + integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== dependencies: - regenerate "^1.4.0" - regenerate-unicode-properties "^8.2.0" - regjsgen "^0.5.1" - regjsparser "^0.6.4" - unicode-match-property-ecmascript "^1.0.4" - unicode-match-property-value-ecmascript "^1.2.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^9.0.0" + regjsgen "^0.5.2" + regjsparser "^0.7.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" -regjsgen@^0.5.1: +regjsgen@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== -regjsparser@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272" - integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw== +regjsparser@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968" + integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== dependencies: jsesc "~0.5.0" @@ -8555,9 +8400,9 @@ remove-trailing-separator@^1.0.1: integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= repeat-element@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" - integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== repeat-string@^1.6.1: version "1.6.1" @@ -8595,6 +8440,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -8643,19 +8493,12 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" - integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== - dependencies: - is-core-module "^2.1.0" - path-parse "^1.0.6" - -resolve@^1.1.7, resolve@^1.3.2: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== +resolve@1.20.0, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.3.2: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: + is-core-module "^2.2.0" path-parse "^1.0.6" restore-cursor@^3.1.0: @@ -8671,11 +8514,6 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -8687,19 +8525,9 @@ reusify@^1.0.4: integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" - integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== - -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== rifm@^0.7.0: version "0.7.0" @@ -8708,13 +8536,6 @@ rifm@^0.7.0: dependencies: "@babel/runtime" "^7.3.1" -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -8722,20 +8543,12 @@ rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.3: dependencies: glob "^7.1.3" -ripemd160@^2.0.0, ripemd160@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" - integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: - hash-base "^3.0.0" - inherits "^2.0.1" - -rollup@2.38.4: - version "2.38.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.38.4.tgz#1b84ea8728c73b1a00a6a6e9c630ec8c3fe48cea" - integrity sha512-B0LcJhjiwKkTl79aGVF/u5KdzsH8IylVfV56Ut6c9ouWLJcUK17T83aZBetNYSnZtXf2OHD4+2PbmRW+Fp5ulg== - optionalDependencies: - fsevents "~2.3.1" + glob "^7.1.3" run-async@^2.4.0: version "2.4.1" @@ -8743,37 +8556,32 @@ run-async@^2.4.0: integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== run-parallel@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" - integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== - -run-queue@^1.0.0, run-queue@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" - integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= - dependencies: - aproba "^1.1.1" - -rxjs@6.6.3, rxjs@^6.5.3, rxjs@^6.6.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== dependencies: - tslib "^1.9.0" + queue-microtask "^1.2.2" -rxjs@~6.6.7: +rxjs@6.6.7, rxjs@^6.5.3, rxjs@~6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" +rxjs@^7.2.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" + integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w== + dependencies: + tslib "~2.1.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -8790,23 +8598,20 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.1.1.tgz#4ddd5a3d7638e7949065dd6e9c7c04037f7e663d" - integrity sha512-W6gVDXAd5hR/WHsPicvZdjAWHBcEJ44UahgxcIE196fW2ong0ZHMPO1kZuI5q0VlvMQZh32gpv69PLWQm70qrw== +sass-loader@12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.1.0.tgz#b73324622231009da6fba61ab76013256380d201" + integrity sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg== dependencies: klona "^2.0.4" - loader-utils "^2.0.0" neo-async "^2.6.2" - schema-utils "^3.0.0" - semver "^7.3.2" -sass@1.32.6: - version "1.32.6" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.6.tgz#e3646c8325cd97ff75a8a15226007f3ccd221393" - integrity sha512-1bcDHDcSqeFtMr0JXI3xc/CXX6c4p0wHHivJdru8W7waM7a1WjKMm4m/Z5sY7CbVw4Whi2Chpcw6DFfSWwGLzQ== +sass@1.36.0: + version "1.36.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.36.0.tgz#5912ef9d5d16714171ba11cb17edb274c4bbc07e" + integrity sha512-fQzEjipfOv5kh930nu3Imzq3ie/sGDc/4KtQMJlt7RRdrkQSfe37Bwi/Rf/gfuYHsIuE1fIlDMvpyMcEwjnPvg== dependencies: - chokidar ">=2.0.0 <4.0.0" + chokidar ">=3.0.0 <4.0.0" saucelabs@^1.5.0: version "1.5.0" @@ -8820,10 +8625,10 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== +scheduler@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" + integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -8853,30 +8658,35 @@ schema-utils@^2.6.5, schema-utils@^2.7.0: ajv "^6.12.4" ajv-keywords "^3.5.2" -schema-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" - integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== dependencies: - "@types/json-schema" "^7.0.6" + "@types/json-schema" "^7.0.8" ajv "^6.12.5" ajv-keywords "^3.5.2" -screenfull@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.1.0.tgz#85c13c70f4ead4c1b8a935c70010dfdcd2c0e5c8" - integrity sha512-dYaNuOdzr+kc6J6CFcBrzkLCfyGcMg+gWkJ8us93IQ7y1cevhQAugFsaCdMHb6lw8KV3xPzSxzH7zM1dQap9mA== +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7" + integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.8.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.0.0" + +screenfull@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-6.0.0.tgz#6f7f37bfe19c5ee50931ac438c1fe380d5b5ff44" + integrity sha512-LGY0nhNQkC4FX4DT4pZdJ5cZH5EOz9Gfh9KcVMl779pS677k4IV1Wv7sY/CwC9VKFT21fYgCh7zkTVVefi5XKA== select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= - selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: version "3.6.0" resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" @@ -8901,47 +8711,28 @@ semver-dsl@^1.0.1: dependencies: semver "^5.3.0" -semver-intersect@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/semver-intersect/-/semver-intersect-1.4.0.tgz#bdd9c06bedcdd2fedb8cd352c3c43ee8c61321f3" - integrity sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ== - dependencies: - semver "^5.0.0" - semver@7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.3.4: - version "7.3.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" - integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== +semver@7.3.5, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== dependencies: lru-cache "^6.0.0" -semver@^5.0.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0: +semver@^5.3.0, semver@^5.5.0, semver@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@^6.0.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.0.0, semver@^7.1.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== - -semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -8961,17 +8752,10 @@ send@0.17.1: range-parser "~1.2.1" statuses "~1.5.0" -serialize-javascript@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" - integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" @@ -9018,11 +8802,6 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" - integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= - setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" @@ -9033,14 +8812,6 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== -sha.js@^2.4.0, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== - dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" - shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -9071,9 +8842,9 @@ sigmund@^1.0.1: integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= signal-exit@^3.0.0, signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + version "3.0.6" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" + integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== slash@^2.0.0: version "2.0.0" @@ -9086,9 +8857,9 @@ slash@^3.0.0: integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== smart-buffer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" - integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== snapdragon-node@^2.0.1: version "2.1.1" @@ -9120,12 +8891,12 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socket.io-adapter@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527" - integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== +socket.io-adapter@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz#4d6111e4d42e9f7646e365b4f578269821f13486" + integrity sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ== -socket.io-parser@~4.0.3: +socket.io-parser@~4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== @@ -9134,52 +8905,49 @@ socket.io-parser@~4.0.3: component-emitter "~1.3.0" debug "~4.3.1" -socket.io@^3.1.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a" - integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw== +socket.io@^4.2.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.0.tgz#8140a0db2c22235f88a6dceb867e4d5c9bd70507" + integrity sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ== dependencies: - "@types/cookie" "^0.4.0" - "@types/cors" "^2.8.8" - "@types/node" ">=10.0.0" accepts "~1.3.4" base64id "~2.0.0" - debug "~4.3.1" - engine.io "~4.1.0" - socket.io-adapter "~2.1.0" - socket.io-parser "~4.0.3" + debug "~4.3.2" + engine.io "~6.1.0" + socket.io-adapter "~2.3.3" + socket.io-parser "~4.0.4" sockjs-client@^1.5.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.1.tgz#256908f6d5adfb94dabbdbd02c66362cca0f9ea6" - integrity sha512-VnVAb663fosipI/m6pqRXakEOw7nvd7TUgdr3PlR/8V2I95QIdwT8L4nMxhyU8SmDBHYXU1TOElaKOmKLfYzeQ== + version "1.5.2" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.5.2.tgz#4bc48c2da9ce4769f19dc723396b50f5c12330a3" + integrity sha512-ZzRxPBISQE7RpzlH4tKJMQbHM9pabHluk0WBaxAQ+wm/UieeBVBou0p4wVnSQGN9QmpAZygQ0cDIypWuqOFmFQ== dependencies: debug "^3.2.6" eventsource "^1.0.7" faye-websocket "^0.11.3" inherits "^2.0.4" json3 "^3.3.3" - url-parse "^1.5.1" + url-parse "^1.5.3" sockjs@^0.3.21: - version "0.3.21" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.21.tgz#b34ffb98e796930b60a0cfa11904d6a339a7d417" - integrity sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw== + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== dependencies: faye-websocket "^0.11.3" - uuid "^3.4.0" + uuid "^8.3.2" websocket-driver "^0.7.4" -socks-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60" - integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA== +socks-proxy-agent@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz#e664e8f1aaf4e1fb3df945f09e3d94f911137f87" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== dependencies: - agent-base "6" - debug "4" - socks "^2.3.3" + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" -socks@^2.3.3: +socks@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== @@ -9187,7 +8955,7 @@ socks@^2.3.3: ip "^1.1.5" smart-buffer "^4.1.0" -source-list-map@^2.0.0, source-list-map@^2.0.1: +source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== @@ -9197,17 +8965,19 @@ source-map-js@^0.6.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== -source-map-loader@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-1.1.3.tgz#7dbc2fe7ea09d3e43c51fd9fc478b7f016c1f820" - integrity sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA== +source-map-js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" + integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA== + +source-map-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-3.0.0.tgz#f2a04ee2808ad01c774dea6b7d2639839f3b3049" + integrity sha512-GKGWqWvYr04M7tn8dryIWvb0s8YM41z82iQv01yBtIylgxax0CwvSy6gc2Y02iuXwEfGWRlMicH0nvms9UZphw== dependencies: abab "^2.0.5" iconv-lite "^0.6.2" - loader-utils "^2.0.0" - schema-utils "^3.0.0" - source-map "^0.6.1" - whatwg-mimetype "^2.3.0" + source-map-js "^0.6.2" source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: version "0.5.3" @@ -9220,15 +8990,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - -source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@^0.5.5, source-map-support@~0.5.12, source-map-support@~0.5.19: +source-map-support@0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== @@ -9236,6 +8998,14 @@ source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@^0.5.5 buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@^0.5.5, source-map-support@~0.5.19, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-support@~0.4.0: version "0.4.18" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" @@ -9244,9 +9014,9 @@ source-map-support@~0.4.0: source-map "^0.5.6" source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" @@ -9263,7 +9033,7 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: +sourcemap-codec@1.4.8, sourcemap-codec@^1.4.4, sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -9291,12 +9061,10 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" -speed-measure-webpack-plugin@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.4.2.tgz#1608e62d3bdb45f01810010e1b5bfedefedfa58f" - integrity sha512-AtVzD0bnIy2/B0fWqJpJgmhcrfWFhBlduzSo0uwplr/QvB33ZNZj2NEth3NONgdnZJqicK0W0mSxnLSbsVCDbw== - dependencies: - chalk "^4.1.0" +splaytree@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/splaytree/-/splaytree-3.1.0.tgz#17d4a0108a6da3627579690b7b847241e18ddec8" + integrity sha512-gvUGR7xnOy0fLKTCxDeUZYgU/I1Tdf8M/lM1Qrf8L2TIOR5ipZjGk02uYcdv0o2x7WjVRgpm3iS2clLyuVAt0Q== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -9335,17 +9103,10 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== - dependencies: - figgy-pudding "^3.5.1" - -ssri@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.0.tgz#79ca74e21f8ceaeddfcb4b90143c458b8d988808" - integrity sha512-aq/pz989nxVYwn16Tsbj1TqFpD5LLrQxHf5zaHuieFV+R0Bbr4y8qUsOA45hXT/N4/9UNXTarBjnjVmjSOVaAA== +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== dependencies: minipass "^3.1.1" @@ -9367,38 +9128,6 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -stream-browserify@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" - integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== - dependencies: - inherits "~2.0.1" - readable-stream "^2.0.2" - -stream-each@^1.1.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" - integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== - dependencies: - end-of-stream "^1.1.0" - stream-shift "^1.0.0" - -stream-http@^2.7.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" - integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== - dependencies: - builtin-status-codes "^3.0.0" - inherits "^2.0.1" - readable-stream "^2.3.6" - to-arraybuffer "^1.0.0" - xtend "^4.0.0" - -stream-shift@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" - integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== - streamroller@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-2.2.4.tgz#c198ced42db94086a6193608187ce80a5f2b0e53" @@ -9417,13 +9146,14 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" string-width@^3.0.0, string-width@^3.1.0: version "3.1.0" @@ -9434,32 +9164,7 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string_decoder@^1.0.0, string_decoder@^1.1.1: +string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -9480,13 +9185,6 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -9494,12 +9192,12 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^5.0.0" + ansi-regex "^5.0.1" strip-bom@^3.0.0: version "3.0.0" @@ -9511,13 +9209,10 @@ strip-eof@^1.0.0: resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= -style-loader@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" - integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" +style-loader@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.2.1.tgz#63cb920ec145c8669e9a50e92961452a1ef5dcde" + integrity sha512-1k9ZosJCRFaRbY6hH49JFlRB0fVSbmnyq1iTPjNxUmGVjBNEmwrrHPenhlp+Lgo51BojHSf6pl2FcqYaN3PfVg== stylehacks@^5.0.1: version "5.0.1" @@ -9527,16 +9222,14 @@ stylehacks@^5.0.1: browserslist "^4.16.0" postcss-selector-parser "^6.0.4" -stylus-loader@4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-4.3.3.tgz#381bb6341272ac50bcdfd0b877707eac99b6b757" - integrity sha512-PpWB5PnCXUzW4WMYhCvNzAHJBjIBPMXwsdfkkKuA9W7k8OQFMl/19/AQvaWsxz2IptxUlCseyJ6TY/eEKJ4+UQ== +stylus-loader@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-6.1.0.tgz#7a3a719a27cb2b9617896d6da28fda94c3ed9762" + integrity sha512-qKO34QCsOtSJrXxQQmXsPeaVHh6hMumBAFIoJTcsSr2VzrA6o/CW9HCGR8spCjzJhN8oKQHdj/Ytx0wwXyElkw== dependencies: - fast-glob "^3.2.4" + fast-glob "^3.2.5" klona "^2.0.4" - loader-utils "^2.0.0" normalize-path "^3.0.0" - schema-utils "^3.0.0" stylus@0.54.8: version "0.54.8" @@ -9571,67 +9264,57 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" -svgo@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.3.0.tgz#6b3af81d0cbd1e19c83f5f63cec2cb98c70b5373" - integrity sha512-fz4IKjNO6HDPgIQxu4IxwtubtbSfGEAJUq/IXyTPIkGhWck/faiiwfkvsB8LnBkKLvSoyNNIY6d13lZprJMc9Q== +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: - "@trysound/sax" "0.1.1" - chalk "^4.1.0" - commander "^7.1.0" - css-select "^3.1.2" - css-tree "^1.1.2" + has-flag "^4.0.0" + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" csso "^4.2.0" + picocolors "^1.0.0" stable "^0.1.8" -symbol-observable@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" - integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== - -symbol-observable@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-3.0.0.tgz#eea8f6478c651018e059044268375c408c15c533" - integrity sha512-6tDOXSHiVjuCaasQSWTmHUWn4PuG7qa3+1WT031yTc/swT7+rLiw3GOrFxaH1E3lLP09dH3bVuVDf2gK5rxG3Q== - -systemjs@0.21.5: - version "0.21.5" - resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-0.21.5.tgz#2fcef4edfe744003da4787f3f3d45d73f94462d2" - integrity sha512-GWzZhN/x7Fsae2CYkz2GF7OgOS+YDgKulcgd5L1kTogZHMKDrPx5T8zI8I0y5RoU9Dx78Z7j1XMfuFa1thD84A== +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== -tapable@^1.0.0, tapable@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== +systemjs-babel@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/systemjs-babel/-/systemjs-babel-0.3.1.tgz#4b864be909b542fb7a84303f24d0dbdbc35da2e1" + integrity sha512-ggZ+v7RSKmN1OlAd46REpfZJmBTnrBUH1zgHnSalIB0KPepfPXEm1p2auE6v7wCmJl231EyApRxnw6X3SgpQtA== -tapable@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" - integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== +systemjs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.11.0.tgz#8df8e74fc05822e6c40170aa409b9ca64833315f" + integrity sha512-7YPIY44j+BoY+E6cGBSw0oCU8SNTTIHKZgftcBdwWkDzs/M86Fdlr21FrzAyph7Zo8r3CFGscyFe4rrBtixrBg== -tar@^6.0.2: - version "6.0.5" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" - integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -tar@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" - integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== +tar@^6.0.2, tar@^6.1.0: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -9640,76 +9323,59 @@ tar@^6.1.0: mkdirp "^1.0.3" yallist "^4.0.0" -terser-webpack-plugin@4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-4.2.3.tgz#28daef4a83bd17c1db0297070adc07fc8cfc6a9a" - integrity sha512-jTgXh40RnvOrLQNgIkwEKnQ8rmHjHK4u+6UBEi+W+FPmvb+uo+chJXntKe7/3lW5mNysgSWD60KyesnhW8D6MQ== +terser-webpack-plugin@5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1" + integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA== dependencies: - cacache "^15.0.5" - find-cache-dir "^3.3.1" - jest-worker "^26.5.0" - p-limit "^3.0.2" + jest-worker "^27.0.2" + p-limit "^3.1.0" schema-utils "^3.0.0" - serialize-javascript "^5.0.1" + serialize-javascript "^6.0.0" source-map "^0.6.1" - terser "^5.3.4" - webpack-sources "^1.4.3" + terser "^5.7.0" -terser-webpack-plugin@^1.4.3: - version "1.4.5" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" - integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== +terser-webpack-plugin@^5.1.3: + version "5.2.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz#ce65b9880a0c36872555c4874f45bbdb02ee32c9" + integrity sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g== dependencies: - cacache "^12.0.2" - find-cache-dir "^2.1.0" - is-wsl "^1.1.0" - schema-utils "^1.0.0" - serialize-javascript "^4.0.0" + jest-worker "^27.0.6" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" source-map "^0.6.1" - terser "^4.1.2" - webpack-sources "^1.4.0" - worker-farm "^1.7.0" + terser "^5.7.2" -terser@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.5.1.tgz#540caa25139d6f496fdea056e414284886fb2289" - integrity sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ== +terser@5.7.1: + version "5.7.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784" + integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg== dependencies: commander "^2.20.0" source-map "~0.7.2" source-map-support "~0.5.19" -terser@^4.1.2: - version "4.8.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" - integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== +terser@^5.7.0, terser@^5.7.2: + version "5.10.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" + integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== dependencies: commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" + source-map "~0.7.2" + source-map-support "~0.5.20" -terser@^5.3.4: - version "5.7.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.0.tgz#a761eeec206bc87b605ab13029876ead938ae693" - integrity sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g== +text-segmentation@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.2.tgz#1f828fa14aa101c114ded1bda35ba7dcc17c9858" + integrity sha512-uTqvLxdBrVnx/CFQOtnf8tfzSXFm+1Qxau7Xi54j4OPTZokuDOX8qncQzrg2G8ZicAMOM8TgzFAYTb+AqNO4Cw== dependencies: - commander "^2.20.0" - source-map "~0.7.2" - source-map-support "~0.5.19" + utrie "^1.0.1" text-table@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through2@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - through@X.X.X, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -9720,23 +9386,11 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timers-browserify@^2.0.4: - version "2.0.11" - resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f" - integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ== - dependencies: - setimmediate "^1.0.4" - timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -9754,13 +9408,6 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9768,10 +9415,12 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" -to-arraybuffer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" - integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= +tmp@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" to-fast-properties@^2.0.0: version "2.0.0" @@ -9833,51 +9482,53 @@ tree-kill@1.2.2: resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== -ts-node@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.0.0.tgz#e7699d2a110cc8c0d3b831715e417688683460b3" - integrity sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg== - dependencies: +ts-node@^10.0.0, ts-node@^10.4.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" arg "^4.1.0" + create-require "^1.1.0" diff "^4.0.1" make-error "^1.1.1" - source-map-support "^0.5.17" yn "3.1.1" -ts-pnp@^1.1.6: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" - integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== - tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + version "3.12.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" + integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" minimist "^1.2.0" strip-bom "^3.0.0" -tslib@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" + integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== tslib@^1.10.0, tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.0.tgz#d624983f3e2c5e0b55307c3dd6c86acd737622c6" - integrity sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw== + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.2.tgz#462295631185db44b21b1ea3615b63cd1c038242" - integrity sha512-wAH28hcEKwna96/UacuWaVspVLkg4x1aDM9JlzqaQTOFczCktkVAb5fmXChgandR1EraDPs2w8P+ozM+oafwxg== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== -tslib@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" - integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== +tslib@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== tslint@~6.1.3: version "6.1.3" @@ -9905,11 +9556,6 @@ tsutils@^2.29.0: dependencies: tslib "^1.8.1" -tty-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" - integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= - tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -9927,10 +9573,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -type-fest@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" - integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" @@ -9940,53 +9586,43 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" - integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= - typeface-roboto@^1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/typeface-roboto/-/typeface-roboto-1.1.13.tgz#9c4517cb91e311706c74823e857b4bac9a764ae5" integrity sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw== -typescript@4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== - -typescript@~4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.3.tgz#153bbd468ef07725c1df9c77e8b453f8d36abba5" - integrity sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg== +typescript@4.3.5, typescript@~4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" + integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== -ua-parser-js@^0.7.23: - version "0.7.28" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" - integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g== +ua-parser-js@^0.7.30: + version "0.7.31" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6" + integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ== -unicode-canonical-property-names-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" - integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== -unicode-match-property-ecmascript@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" - integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg== +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== dependencies: - unicode-canonical-property-names-ecmascript "^1.0.4" - unicode-property-aliases-ecmascript "^1.0.4" + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531" - integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ== +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== -unicode-property-aliases-ecmascript@^1.0.4: - version "1.1.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz#dd57a99f6207bedff4628abefb94c50db941c8f4" - integrity sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg== +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== union-value@^1.0.0: version "1.0.1" @@ -10003,11 +9639,6 @@ uniq@^1.0.1: resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -10022,15 +9653,6 @@ unique-slug@^2.0.0: dependencies: imurmurhash "^0.1.4" -universal-analytics@0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.23.tgz#d915e676850c25c4156762471bdd7cf2eaaca8ac" - integrity sha512-lgMIH7XBI6OgYn1woDEmxhGdj8yDefMKg7GkWdeATAlQZFrMrNyxSkpDzY57iY0/6fdlzTbBV03OawvvzG+q7A== - dependencies: - debug "^4.1.1" - request "^2.88.2" - uuid "^3.0.0" - universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -10055,9 +9677,9 @@ upath@^1.1.1: integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" @@ -10066,18 +9688,10 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-parse@^1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -url-parse@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.1.tgz#d5fa9890af8a5e1f274a2c98376510f6425f6e3b" - integrity sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q== +url-parse@^1.4.3, url-parse@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.3.tgz#71c1303d38fb6639ade183c2992c8cc0686df862" + integrity sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ== dependencies: querystringify "^2.1.1" requires-port "^1.0.0" @@ -10100,31 +9714,24 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -util@0.10.3: - version "0.10.3" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" - integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= - dependencies: - inherits "2.0.1" - -util@^0.11.0: - version "0.11.1" - resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" - integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== - dependencies: - inherits "2.0.3" - utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@8.3.2: +utrie@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.1.tgz#e155235ebcbddc89ae09261ab6e773ce61401b2f" + integrity sha512-JPaDXF3vzgZxfeEwutdGzlrNoVFL5UvZcbO6Qo9D4GoahrieUPoMU8GCpVpR7MQqcKhmShIh8VlbEN3PLM3EBg== + dependencies: + base64-arraybuffer "^1.0.1" + +uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0: +uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -10141,11 +9748,6 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -vendors@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -10155,40 +9757,18 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vm-browserify@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" - integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== - void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -watchpack-chokidar2@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" - integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== - dependencies: - chokidar "^2.1.8" - -watchpack@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b" - integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg== +watchpack@^2.2.0, watchpack@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.0.tgz#a41bca3da6afaff31e92a433f4c856a0c25ea0c4" + integrity sha512-MnN0Q1OsvB/GGHETrFeZPQaOelWh/7O+EiFlj8sM9GPjtQkis7k01aAxrg/18kTfoIVcLL+haEVFlXDaSRwKRw== dependencies: + glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" - neo-async "^2.5.0" - optionalDependencies: - chokidar "^3.4.1" - watchpack-chokidar2 "^2.0.0" wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" @@ -10213,9 +9793,9 @@ webdriver-js-extender@2.1.0: selenium-webdriver "^3.0.1" webdriver-manager@^12.1.7: - version "12.1.7" - resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.7.tgz#ed4eaee8f906b33c146e869b55e850553a1b1162" - integrity sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA== + version "12.1.8" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.8.tgz#5e70e73eaaf53a0767d5745270addafbc5905fd4" + integrity sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg== dependencies: adm-zip "^0.4.9" chalk "^1.1.1" @@ -10229,10 +9809,22 @@ webdriver-manager@^12.1.7: semver "^5.3.0" xml2js "^0.4.17" -webpack-dev-middleware@3.7.2, webpack-dev-middleware@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz#0019c3db716e3fa5cecbf64f2ab88a74bab331f3" - integrity sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw== +webpack-dev-middleware@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.0.0.tgz#0abe825275720e0a339978aea5f0b03b140c1584" + integrity sha512-9zng2Z60pm6A98YoRcA0wSxw1EYn7B7y5owX/Tckyt9KGyULTkLtiavjaXlWqOMkM0YtqGgL3PvMOFgyFLq8vw== + dependencies: + colorette "^1.2.2" + mem "^8.1.1" + memfs "^3.2.2" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^3.0.0" + +webpack-dev-middleware@^3.7.2: + version "3.7.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" + integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== dependencies: memory-fs "^0.4.1" mime "^2.4.4" @@ -10287,23 +9879,15 @@ webpack-log@^2.0.0: ansi-colors "^3.0.0" uuid "^3.3.2" -webpack-merge@5.7.3, webpack-merge@^5.7.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.7.3.tgz#2a0754e1877a25a8bbab3d2475ca70a052708213" - integrity sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA== +webpack-merge@5.8.0, webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== dependencies: clone-deep "^4.0.1" wildcard "^2.0.0" -webpack-sources@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac" - integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - -webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: +webpack-sources@^1.2.0, webpack-sources@^1.3.0: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== @@ -10311,6 +9895,11 @@ webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack- source-list-map "^2.0.0" source-map "~0.6.1" +webpack-sources@^3.2.0, webpack-sources@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.2.tgz#d88e3741833efec57c4c789b6010db9977545260" + integrity sha512-cp5qdmHnu5T8wRg2G3vZZHoJPN14aqQ89SyQ11NpGH5zEMDCclt49rzo+MaRazk7/UeILhAI+/sEtcM+7Fr0nw== + webpack-subresource-integrity@1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.5.2.tgz#e40b6578d3072e2d24104975249c52c66e9a743e" @@ -10318,63 +9907,65 @@ webpack-subresource-integrity@1.5.2: dependencies: webpack-sources "^1.3.0" -webpack@4.44.2: - version "4.44.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72" - integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" +webpack@5.50.0: + version "5.50.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.50.0.tgz#5562d75902a749eb4d75131f5627eac3a3192527" + integrity sha512-hqxI7t/KVygs0WRv/kTgUW8Kl3YC81uyWQSo/7WUs5LsuRw0htH/fCwbVBGCuiX/t4s7qzjXFcf41O8Reiypag== + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.50" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^4.3.0" - eslint-scope "^4.0.3" + enhanced-resolve "^5.8.0" + es-module-lexer "^0.7.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.4" json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.7.4" - webpack-sources "^1.4.1" - -webpack@^4.46.0: - version "4.46.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" - integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== - dependencies: - "@webassemblyjs/ast" "1.9.0" - "@webassemblyjs/helper-module-context" "1.9.0" - "@webassemblyjs/wasm-edit" "1.9.0" - "@webassemblyjs/wasm-parser" "1.9.0" - acorn "^6.4.1" - ajv "^6.10.2" - ajv-keywords "^3.4.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.2.0" + webpack-sources "^3.2.0" + +webpack@^5.64.4: + version "5.64.4" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.64.4.tgz#e1454b6a13009f57cc2c78e08416cd674622937b" + integrity sha512-LWhqfKjCLoYJLKJY8wk2C3h77i8VyHowG3qYNZiIqD6D0ZS40439S/KVuc/PY48jp2yQmy0mhMknq8cys4jFMw== + dependencies: + "@types/eslint-scope" "^3.7.0" + "@types/estree" "^0.0.50" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^4.5.0" - eslint-scope "^4.0.3" + enhanced-resolve "^5.8.3" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.4" json-parse-better-errors "^1.0.2" - loader-runner "^2.4.0" - loader-utils "^1.2.3" - memory-fs "^0.4.1" - micromatch "^3.1.10" - mkdirp "^0.5.3" - neo-async "^2.6.1" - node-libs-browser "^2.2.1" - schema-utils "^1.0.0" - tapable "^1.1.3" - terser-webpack-plugin "^1.4.3" - watchpack "^1.7.4" - webpack-sources "^1.4.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.3.0" + webpack-sources "^3.2.2" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" @@ -10390,11 +9981,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -10415,31 +10001,17 @@ which@^2.0.2: isexe "^2.0.0" wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: - string-width "^1.0.2 || 2" + string-width "^1.0.2 || 2 || 3 || 4" wildcard@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -worker-farm@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" - integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== - dependencies: - errno "~0.1.7" - -worker-plugin@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-5.0.0.tgz#113b5fe1f4a5d6a957cecd29915bedafd70bb537" - integrity sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ== - dependencies: - loader-utils "^1.1.0" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -10473,16 +10045,16 @@ wrappy@1: integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= ws@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" - integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== dependencies: async-limiter "~1.0.0" -ws@~7.4.2: - version "7.4.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" - integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== +ws@~8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" + integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== xml2js@^0.4.17: version "0.4.23" @@ -10497,15 +10069,10 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== -xtend@^4.0.0, xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== y18n@^5.0.5: version "5.0.8" @@ -10517,17 +10084,12 @@ yallist@^2.1.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.10.0: +yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -10549,11 +10111,16 @@ yargs-parser@^18.1.2: decamelize "^1.2.0" yargs-parser@^20.2.2: - version "20.2.7" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs@^13.2.4, yargs@^13.3.2: +yargs-parser@^21.0.0: + version "21.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.0.tgz#a485d3966be4317426dd56bdb6a30131b281dc55" + integrity sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA== + +yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== @@ -10586,7 +10153,7 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.1.1, yargs@^16.2.0: +yargs@^16.1.1: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -10599,11 +10166,29 @@ yargs@^16.1.1, yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.0.0: + version "17.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.0.tgz#295c4ffd0eef148ef3e48f7a2e0f58d0e4f26b1c" + integrity sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yn@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zone.js@~0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16"