Commit 36dff7d169fc5015eb913c9c205b7c2b895f1ad5

Authored by Volodymyr Babak
2 parents 1f44d538 07dca7ef

Merge branch 'master' of github.com:thingsboard/thingsboard

Showing 25 changed files with 2677 additions and 217 deletions

Too many changes to show.

To preserve performance only 25 of 421 files are displayed.

... ... @@ -360,6 +360,7 @@
360 360 </systemPropertyVariables>
361 361 <excludes>
362 362 <exclude>**/sql/*Test.java</exclude>
  363 + <exclude>**/psql/*Test.java</exclude>
363 364 <exclude>**/nosql/*Test.java</exclude>
364 365 </excludes>
365 366 <includes>
... ...
... ... @@ -19,8 +19,8 @@
19 19 "templateHtml": "<tb-alarms-table-widget \n [ctx]=\"ctx\">\n</tb-alarms-table-widget>",
20 20 "templateCss": "",
21 21 "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",
22   - "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}",
23   - "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}",
  22 + "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}",
  23 + "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}",
24 24 "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}"
25 25 }
26 26 }
... ...
... ... @@ -55,8 +55,8 @@
55 55 "templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>",
56 56 "templateCss": "",
57 57 "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}",
58   - "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}",
59   - "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}",
  58 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"identifyDeviceSelector\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}",
  59 + "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}",
60 60 "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\"}"
61 61 }
62 62 },
... ... @@ -127,8 +127,8 @@
127 127 "templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
128 128 "templateCss": "",
129 129 "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",
130   - "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}",
131   - "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}",
  130 + "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}",
  131 + "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}",
132 132 "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;\"}]}]}"
133 133 }
134 134 },
... ... @@ -145,7 +145,7 @@
145 145 "templateHtml": "<tb-entities-hierarchy-widget \n [ctx]=\"ctx\">\n</tb-entities-hierarchy-widget>",
146 146 "templateCss": "",
147 147 "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",
148   - "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}",
  148 + "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}",
149 149 "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {},\n \"required\": []\n },\n \"form\": []\n}",
150 150 "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 += \\\" <b>\\\"+ data['temperature'] +\\\" °C</b>\\\";\\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\":{}}"
151 151 }
... ... @@ -162,15 +162,15 @@
162 162 "resources": [],
163 163 "templateHtml": "<tb-qrcode-widget \n [ctx]=\"ctx\">\n</tb-qrcode-widget>",
164 164 "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}",
165   - "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",
166   - "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",
  165 + "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",
  166 + "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",
167 167 "dataKeySettingsSchema": "{}\n",
168   - "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\"}"
  168 + "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\"}"
169 169 }
170 170 },
171 171 {
172 172 "alias": "markdown_card",
173   - "name": "Markdown Card",
  173 + "name": "Markdown/HTML Card",
174 174 "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAYAAABJ/yOpAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJztnXl8E9Xax79pku4bLbuyVJGCQClgK2WnQAVFlgKCgvQqoiAKCO53ebkovF5fAVEvKiCIiEJBEES2Fig7CAUr+9qN0oWmadM2SbPN+0dsaNpM2pSCLPP9fPh86JkzZ54zk2fmnDO/eR6ZIAiCyWRCrVaj1+uxWCxISNyvuLm54enpSWBgIEqlEpnRaBRycnLw9PRELpff9AHq1atXB2ZKSPw1mM1mSktL0Wg0NG7cGIVara4z55CQuNuRy+X4+/sDoFarcdPr9ZJzSEhUwsfHB71ej5s055CQqIpcLsdiseD2VxsiIXEnIznInxiNRk6ePFltvXPnzqHVam+DRc4RBMHp9jvFzrudWjmITqdj165dLu2zefNmBEHg4MGDqFSqWpdXRq/X8/PPP9v+3rhxIzqdziXbALKysnj77berrffGG2+Ql5fncvt1SUZGBo8//jhpaWmidW7GTovFwpkzZ2pp3b2Fyw5y/vx5Jk+ezFdffVXjffR6PZ9//jkymYxPPvnEdvdztdwRmZmZvP/++2RkZJCVlcV7771HZmamq926q2jYsCETJ06kcePGt6T9kpISJk6ceEvavttQuFLZbDazYsUKnn32Wb755psa7ZOYmMixY8cAWLJkCRkZGfz+++8ALpX379/fYfs5OTl06dKFxMREFAoFnTt3Jj8/n9atW7N+/Xr27t2Lj48PsbGxdOnSBYDDhw+ze/durl+/jkKhYMaMGXZtGo1GvvnmG4YOHUqTJk1ISUlh7dq1yOVyNBqNrd6lS5f4/vvvkclkjB07llatWrF27Vr69OlDgwYNACgoKCAxMZGBAweyZMkSrl27ho+PD88++yxt27YVPW9i7XTo0IEtW7YAoFKpeOCBB2z7iNl5/vx5Vq9ebVvbnzRpEn5+fmRkZLBixQq0Wi2xsbFERERw9epVvvvuO0pLS5k3bx4AI0eOpEWLFuIX+R7GpSeIXC5n7ty5tG/fvsb71K9fH7VaTXh4OGazmZCQEOrXr+9yuRgFBQX06tWLI0eOcOjQIXr16sX169cBaNeuHa+99hp9+vRhypQplJaWArBs2TJ8fHwYP348o0aNIjAw0NZeWVkZ06dPR6lU0qRJE7Kzs5kyZQq9e/emV69eGI1GAPLz85kwYQLdunXj8ccfZ8KECahUKn777TeuXLlCbm4u2dnZpKenc+TIEQoKCvjll18YP348nTp14uWXX7a15Qixdho0aEBUVBT79+8nOzvbVl/MzuzsbF566SW6du1KXFwcv/76KyqVCpVKxYQJE+jatSuxsbG8/fbbZGVl4e/vT2RkJO7u7kRFRREVFWV3fu43XHqC1Ibw8HDWrl1LTEwMeXl5dO/enfDwcACXyx1RWFiIj48PgYGBmM1mfH19UavVADRt2pTDhw9jNpvx9/fn2rVrPPLIIwA89thjVdrVarW89NJLxMbGMnz4cAB27dpFdHQ0AwYMACA4OBiAhIQEevfuTUxMDAD79+8nMTGRpk2bkp2dzfbt29HpdPTs2ZPmzZsD4OXlRceOHenYsSOLFy8mKyuLli1bOuyXWDvlN5GAgAC7+mJ2JiYm0q9fP5544gmbDeX1mzVrhtlsRqVSERISwpEjR4iNjSUyMhKlUkm3bt3EL+x9wi13kP/+978kJiZy+fJl8vPz8fDwQKlUArhUPmXKFIftFxcX06hRI95++20EQWD37t2o1WrUajUjR45k0KBBBAcHo9PpMJvNTm01m83o9Xq7OYxWq8XPz8/hccvfuAIEBASg0WgICQkhMzOT1NRUDAYDqampPPTQQ1X29/b2Rq/Xi9pS03aqs9NgMNicoiJqtRqTyWTra1RUFA8//LBo+/crt3yZ9/nnn8fb25vFixcTHBzMJ598wosvvuhyuRilpaUolUqCg4OpX78+SqUSrVbL5cuXadCgAW+++SYvvPACTZo0qdZWPz8/Vq5cyZkzZ2zj7/DwcPbs2VNlZaxjx44cPHgQo9GIwWDgwIEDhIeH065dO9avX0+3bt3o0aMH8fHxtGnTxuXzVl07vr6+FBQU2P4WszMyMpJt27Zx9uxZzpw5Y5ubdOrUiaKiIsaOHcvEiROZOHEiHTt2BKzOW1pa6tSB7xdcfoL8+9//5sSJE6hUKkaOHMmsWbOczkmysrJ45JFH8PPzIy8vjzZt2iCTyUhLS3OpXAytVotCcaMbCoWC0tJS2rdvj5eXF0OGDKF+/fqkpqbWSFLj6enJZ599xrhx4/j5558ZNmwY0dHRDBo0iIYNG5KXl4dCoeDxxx/n8ccfZ9CgQVgsFgYOHEhERARms5mSkhKGDx+OyWRi+fLlPPTQQy6vrD300EMO2yln6NChzJo1iy+//JJp06bRp08fh3Z26NCBN954g//85z/4+vpiNBqRy+VERETwxBNP8MQTT9CkSRNKSkrYtGkTCoUChULB008/zVNPPUVAQADTpk2jd+/eLtl/ryBLS0sTKg4VbpY7Tc1bVFREWVkZ9erVsw3haoPBYKCwsLBKO1qtFplM5nAYc6vR6XTodDqCgoJE7bRYLJhMJtzd3cnOzmbYsGHs37/f1ofyOYij86PRaJDJZA6HbvcD6enpt34O8ldTeTJbW9zd3WnYsGGVcm9v7zppvzZ4eXlVcczKdubk5DB+/Hjc3NxQKBT8z//8j50jyOVyh/0CqMsb593KPe8g9ztNmzYlMTHxrzbjrkXSYklIOMHlJ8jRo0c5ePAgAQEBDBkyxG78KyFxr+HSEyQ3N5cff/yRsLAwioqKmDlz5q2yS0LijqDWq1hGo5F+/fqRlJSEm9sNP7vTVrEkJGpLenp67ecgiYmJPPbYY3bOISFxr1GrVaxjx46xbNkyvvjii7q2R0LijsJlBzl37hxz585l/vz5NGrU6FbYJCFxx+DS+EgQBD744AP++c9/iqpQJSTuJVx6guTm5pKWlsacOXNsZW+88Qbdu3evc8MkJO4EXHKQxo0bc+DAgVtli4TEHYe0BCUh4QTJQSQknHDPOsjdFudKjOriX0ncWlxyEIvFwp49e/jiiy/47rvvKCwsrPG+Z86c4dSpU9WW3ytxrmraX2fUJP6VxK3FZQc5deoUbdu25dq1a7z//vvV7mM2m8nIyOD48eMcO3aMq1evYjKZRMvv9jhXrvbXGbc6/pVE9bi0iqVQKGzBEzp16sSYMWOq3SczM5OpU6fi5eWFIAisX7+ehQsXIpPJHJbf7XGuXO1vSEiIw/N29uxZ0fhXycnJrFu3Dp1OxyOPPCIa0KIcsfMmUT21moPs2LGDjz76yGkwhXJatmzJ0qVL8fDwwNvbmyVLlhASEiJafrfHuXK1v2KIxb/S6/W89tprDB48mKlTpxIREVHtNRA7bxLVUysHuX79OgaDgT/++KNGKdu2bdvG4MGDeeqpp9i+fbvT8opxrnx8fKrEuUpNTbWLc1VOeZyriIgIPD09gRtxrvr378+ECRMA+/hR/fr1cxjnauDAgfTs2dMuztXXX3/NZ599RnZ2dpU4V8OHD8fX15esrCyX+ytG/fr16datW5VPht3d3alXrx6HDh3Cw8ODyMjIas+/s/Mm4ZxaiRXHjh3Lc889R1xcHCdPnrSFixFj/PjxNS6/F+JcudJfV3Fzc2P16tWsXr3a9sT717/+JVq/NudN4gYuT9LLMRqNaLXaOg9acK/GuaotleNfCYKAp6cnL7/8Mps3byYhIYGMjAzR/Wtz3iRu4NITJDU1lffee48GDRqQlZVF//79baE864p7Nc5Vbakc/+rRRx9lzJgxPPDAAxQVFdGmTRuaNm0qun9tz5uEFZe/KDQYDOTn51O/fn3c3d2rbL/VXxTey3GuxKgc/8pkMqFSqfD29q5xzKq6Om/3E+np6fd+4DgJidpyU5/cSkjcD9wWB8nJyWHdunWsW7eOEydOANb3DgcPHuTgwYN2L+vKqakGqaaaq5vlTrOnptxp9txt1NpBLl++zKFDh2pU98KFC8THx9v9yK5du8a2bdt49913uXDhgl19VzRIzjRXdZVr706zxxVqqkmTcEytHOTq1avMmDGDtWvX1nifZs2aMWrUKDp16gRAWFgYs2fPdvhOoa40SHWVa+9Os0fi9uHyi0Kj0cjcuXOJi4tj//79dW6QMw2So1x7YH0Z9u677+Lh4cHYsWNp3bq1aK693377zaG26plnnnGYs+9W2yOW+y85OZkNGzYQFBRk04xNnz4dPz+/OtGkKRQKtm/fzrhx4wBYvnw5w4YNQ6FQ8OWXX5Kbm0tQUBDjxo27b/MTQi2eICtWrGDAgAFOdUQ3g7McfI5y7QEolUpGjBhBeHg4L730EiaTSTTXnpi2Sixn3622R4yDBw/SpEkT1Gq1TTu1evVqoG40aSqVig0bNti2x8fHU1RUhEqlYsuWLcTFxdGmTRvGjRvnNA33vY5LT5CcnByOHTvGokWLSElJuSUGieXgE8u1B9a3zREREURERPDVV1+RlZVFixYtHObaE8v95yxn3620xxkNGzYkMDCQ4uJiWrZsyfHjx219uNnci87w8vIiLCyMsLAwjh07RmJiIqNHj66RzfcaLjnIxo0bUalUTJ8+nZKSEq5du8aaNWtuy8kTy7VXGV9fX6cfWIlpq7Kzs13K2VdX9tRk/4oZtupKkyaXy6v9HgWsN6yioqJa23+345KDjBkzhsGDBwPWL+M2bNhgu4PWlopq3crlFTVIkZGRTJ06lSFDhiAIgsOl4cpUzLVXrvBt164dCxcu5LnnnsNkMrF69WqWLl1K48aN+fXXXxk7dqxDfdmtsqc6hg4dCliVwGCvrQLYunVrtW2Ua9KmT5/OvHnzmDlzJg0bNiQ7O5uSkhJ8fX3t6ptMJiwWCxaLhUOHDlWZy9xPuOQgAQEBtqFGXl4enp6eN51Du1xrtGjRIluuvYrlFXPwOcq15+zu6SjXXo8ePRxqq+RyuWjOvltpj6u5/+pSk/bcc8/x1FNP0bhxY/Lz81EoFJhMJnJzc3nyyScxGo1ERUXd1+mgb4vUZO/evWzcuNG2elMZrVaLXq+vkmukogapulx7znAl156znH1/hT1i1JW2Sq/XU1paSmBgIHK5nLS0NCZPnsxPP/2E2Wy+b/MTwm3MUejm5sbhw4cZPXo0MTExto+XyvH29nY4rKmYg6+6XHvOcOUG4Cxn319hjxh1lXvR09PT4XDvr8y9eCchiRUlJES4LWLF6jRXdwMnTpywaclycnKqbC8uLrZ9N1+ZzZs3o9Pp7nhNVLmdNS0vp7p+3aq4XrfrfLrsIOfPn2fjxo22f2VlZU7rO9Nc3U0IgkB8fHyVPqjVauLi4mzvKCqSk5PDvHnz8PT0rJVG63Zptyra6azckT3O+nUr43rdLo2Zyw6ya9euGgc+A+eaq7uFTp06MWrUKJo1a1Zl2+zZsxk8eLDD5e7t27cTExNj9x7DEWIardul3RKzs3K5q/bcC3G9XJ6kl5aW0rNnT3r16nXTB3ekKdJoNA7jTYFj7ZNer3eoKapXr56oZslRO35+fg61WM64dOkSFy9eFF2dS0hIYNq0aba/XdFoyeVyh+VBQUGiWiln8bISEhIAGDBgQLV2OioXs1OsX2IaNlevr5+fn2i/NBoNs2fPxmAwMGrUKFvwEEfxzaB28cFcfoIUFBSwd+9eEhISbDGlaosjTZFYvCkx7ZOYpkisfWftONJiOSMxMZEnn3zSYZ5GlUpFeno6nTt3tpW5otESKxfTSlUXL2vZsmUsXbq0RnY6KnemJXPULzENm6vX11m/jEYj3bt3p1evXkyZMoWcnBzR+GZiv4fqcNlBnn/+eSIjI0lKSmLSpEk35SRi8ZocxZuqqH0KCwurkczDUfti7YhpsZyRlZVli5FVmYSEBPr27Wv3Eq9cozV8+HC8vLzIysqy/fDKNVrlsbDEysvPT1hYGCNGjKBHjx4kJiZWGy9rwYIFfPrppzWy01G5M3sc9Ussrle5/TW9vs76FRwcTL9+/YiJiSE6Oppdu3aJxjeD2sUHc9lB2rZtS0xMDHPmzMFkMnHu3DlXmwCsj+Vhw4aRkpJCdna2qKaoPN6UmPZJTFMk1r5YO2q12qbFyszMrFaLBdYLLbbCs2PHDofDmXJuVqNVTrlWqjxelr+/PxMmTGD27Nl29Ro3buww5I+YndXZL4ar/aru+lbXr3L8/PwoLS0VjW9W099blePXuCd/Uh4by2AwoNVq8fDwqNF+lTVXrsZrioyMZNu2bZw9e5YzZ87YlowraooqIta+WDudOnWiqKiIsWPHMnHiRCZOnFhtQLw2bdrYPiGuiEaj4dy5c3Tt2rX6E4O9Rqsm5eVaKZPJxKFDh2jfvn218bJ+/PFHVq1aVSM7xcrF7HFGZQ2bGGLXpSZxwPR6Pfv27SM8PFw0vllt44O5NEnPz89n0qRJNGzYkGvXrhEdHU3r1q1rtG9lzVXXrl1d0hR16NDBofYpICDAoaZITLPUpk0bh+1EREQ41WI5YsCAASxYsICrV6/y4IMP2sp37txJjx49avxmXUyj5ai8RYsWDrVSeXl5TuNlbdu2DYvFwtixY6u1U6xczB5nVNawiSV/Fbu+169fd9ivnJwc8vLyGDt2LFlZWQwZMsQ2P3EU30yv19cuPlhaWppQUFBQ4385OTnCyZMnhezsbIfbnVFaWiqoVCq7ssLCQiE3N1cwGAxO9zWbzUJZWZkgCIJw7do1ITIy0m4fnU4n5OfnCyaTyWn71bVjMplE7ZkxY4awZ88eu7K1a9cKI0eOFDQaja1s0qRJwo4dO5z2xxFFRUV27TgqT01NFQYOHCiUlpZWqWs0GoWcnByHbWi1WkGr1dqVidlZnf1idoqh1WqrXPfKOLsuYv0yGAxCbm6ubb+KlJaWVumvINT89yYIgpCWlia4vMzr7u5u99mpKzjSXNVUU1Sd9klMU1S5/eracaTF+uabb9ixYwdXr16t8sHRyJEjKSwsZOvWrTzzzDNotVqOHz/O/Pnza9SviohJfhyVO9JKKRQK0dz1lcf3YnbWxH5XpUkVNWxiOLsuYv1SKpWiujkxLZmrGjZJiyUhIcIdFTiuprkChVpqe8Q0Rbc6R2HF4+bl5VVZTJC4s6mVg1gsFo4ePcrmzZvrzJCa5AqsrbZHTGtU0+PWlsrHPXjwIHFxcXetaPN+xGUHyc3NZcKECWzatOmm36S7Sm21PTXVRNU1lY87bNgwevTowZw5c26rHRK1x+VJ+qxZsxg9ejQDBw6s8T6O4jU1btxYNFegI5zFpxLT3pRTWWvkSo5CsL4X+OGHH7h48SIeHh5MnjyZZs2aVZsr0JHG6fXXX2fw4MGkpaWJLnlK3Dm49ARJT0/n8uXL5OXl8e2339Z4qOMoXpNYrkAxxLQ9zrQ3UFVT5GqOQr1ez3PPPYdWq2X8+PE8/fTTBAQEVKt9EtM4KRQKnnrqKZv8QeLOxqUnSFpaGg0aNKB58+ZotVqmTJnCihUrqF+/frX7Vo7XVDFXIGDLFSiGWLysitobgP3799vFcaqsKRI7rlg7DzzwAIGBgVUie1gsFptG6Nlnn62ifRLTOIE1sWlycrLT/krcGbj0BFEoFDz44IP06dOHJ598kvDwcIdSi5oglivQVcS0N+VU1hS5mqNQrVY7vAFUpxFypmXS6XTSN993CS45SKtWrTh//jxlZWUIgkB2drYtxq2riOUKLMeRdgiqanvEtDfgWFPkao7CsLAwjh49WkX6LjjRCFWnxUpJSbmteQ4lao9LQ6xGjRoxdOhQ4uLi8Pb2JiQkpEqYy5oSERHhMFdgOY60Q+A4PpUj7Q041hSJHVcsRyHAm2++yZgxY2jUqBEGg4EZM2Y4zRXoTIuVnZ3NgQMH+Pvf/16r8yZxe6nVm/TyIASO9nP1TbpYrsDyO7wjiULlnH3gOLfg5MmTiY2NdTjUcTVHoSAI5OXl4efnZxseieUKFDtueYzc0aNH2yImSty53NM5CrVaLX379iUpKem2JuR0dtz4+HhKS0t54YUXbps9ErXnnnYQCYmb5Y7SYklI3IlIDiIh4QTJQSQknKAA6lRdKilVJe4lFMB9naRRQkIMaZIuIVENkoNISDhBchAJCSdIDiIh4QTJQSQknCA5iISEEyQHkZBwguQgEhJOkBxEQsIJkoNISDhBchAJCSdIDiIh4QTJQSQknHBHOsj58+dZsmQJV69erVH97du3s3z58ltsVd2Rnp6OwWBwuK2wsJAlS5ZIgeXuEGrlIFFRUURGRrJ69eoq286fP09kZCSRkZGcPHmyVkadPHmSefPm1Ti06YYNG/jss89qdazbTUZGBk888QT//e9/HW5XqVTMmzePQ4cO1cnxLly4wJUrV+qkrfuRWjmIRqNBo9Gwdu3aKts2bNhg216TLKL3G02bNmX69Ok8/fTTt+V4r7zyCu+///5tOda9iMvR3csJDAzk9OnTXLhwwZbI02Qy8csvv1CvXj27jLZgjc6+b98+wJrRtDzgXFZWFgcOHCAyMpJDhw7RrFmzKsfKzMy0bYuKisJisZCYmMiVK1fo0qWLQ/sOHz5McnIywcHB9O7d25bVdOPGjQQFBdGzZ08ADhw4gEwmo1u3bgDs3buXwsJChgwZwk8//UTz5s3x9/dn7969+Pn5MXTo0CrhfFQqFTt37iQiIoKQkBAsFgvr1q3j8ccfp0WLFpjNZn766ScefvhhWrduTVBQkF1kx/L87W5ubrRt27ZKXzIyMti1axfu7u7ExMSwa9cuwsLCbNEZCwsL2bJlCxqNhvDwcLp27Yper2fTpk1otVpUKhXx8fEMGTLEYY4UCSekpaVVm8ywMo8++qgwc+ZMoUuXLsJHH31kK09MTBRCQ0OFf/3rX0JoaKhw4sQJQRAEYeXKlUKbNm2EJ598Uhg4cKAQGhoqrFy5UhAEQdixY4cQGhoqdO/eXQgNDRXmzJkjrF27VggNDRX27dsnqFQqISYmRoiKihLS09MFQRCEt956SwgNDRW6desmdOzYUejcubMQHh5us+Of//ynEBoaKkRHRwtdunQRunTpIhw6dEgQBEGYMGGC0L9/f0EQBMFisQi9evUSevbsKVgsFkEQBCE6Olp4+eWXBUEQhPDwcKFPnz5C586dhd69ewuhoaFCXFxclfNRUlIitGvXznYufv/9dyE0NFSYPXu2IAiCcObMGSE0NFRYs2aNkJaWJoSGhgqffvqpIAiCcPHiRSEyMlJo166d0KtXL6FLly5CaGio8Pnnn9va6ty5s9C+fXuhV69eQrdu3YTQ0FBh8eLFgiAIwqVLl4Ru3boJnTt3FgYMGCCEhoYKn3zyiZCfny/0799fePTRR4WwsDChf//+glqtdvla38+kpaUJtZ6ky2QyBg0axKZNmzCZTAD8/PPPhISEEBoaalf3+PHjjBo1is2bN/Prr7/SqlUrvvvuO7s6bdq0YevWrUydOtVWptVqmThxIvn5+SxdupTmzZvzxx9/sGnTJoYOHcr+/fvZu3evXYTFw4cPEx8fz/jx49m5cyeJiYkEBQXx97//HUEQ6N69O5mZmeTm5nLmzBny8vLIy8vj7Nmz5OXlkZWVZXuagDWpZ2JiIklJScTExHD48GGKiorsbPfx8SE8PNw2sd69ezcymYykpCQAW4DvHj16VDmPX3zxBSUlJcTHx7Nnzx7eeecdu+0LFizAbDazadMm9uzZw2uvvWa3fc6cOZjNZrZu3cqOHTuIi4tj2bJl6PV6EhISaNiwIW3btiUhIYHAwEDxCyrhkFo7iF6vJzY2FpVKZRuW7Nmzh5EjR1JWVmZXd/78+YwYMYKVK1fy6aefUlZWZpfDA2D48OGEhITg6+trK/voo484ffo0c+fO5dFHHwXgjz/+AOCZZ55BJpPh7+/Pww8/bNunfBg3btw4wDoUHDJkCFlZWaSnp9O9e3cAkpOT2bVrF23btqVVq1bs3r2b48ePA9jqgDVgd3kwvPKhZH5+fpXzERUVxenTp9FqtezatYuhQ4eSlZXFhQsXSE5OJiQkxC5veTkpKSl06NDB1r+K+UQEQeD3338nKiqKkJAQwBpkuxyDwcDhw4dp2rQpSUlJxMfHIwgCZrOZs2fPVjmWhOvU2kG0Wi2dOnXioYceYsOGDWzatAmz2czQoUPR6/V2defOnctzzz1HcnKyS0k4c3NzkcvlbN++3VZWngRTLJ1v+di+4vby/6tUKlq3bk3jxo1JTk5m9+7dREdH069fP5uDNGrUqEqGqprQo0cPzGYz27dv58KFC7zwwgs8/PDDtnYrOl1FSkpKRO/sJpOJsrIy0b6WlJRgsVjIyclhzZo1rFmzhuTkZNq1a1frZKcS9tR6kl5+AWJjY1m4cCEXL16kT58+1K9f3271SqfTsWrVKoYMGcL//u//Atal4IopDMR46623UKvVfP311/Tu3ZuhQ4faJtupqam2H3LFH0N5hJbLly/b7sYXL15EJpPZFgCioqLYtWsXOTk5fPjhh5hMJhYvXkxhYaHoD7k62rdvT0BAAJ999hkPPPAAoaGh9OvXj3Xr1pGdnS3abtOmTUlNTUUQBGQymV1flEolDRo0IDU11VZWcXtQUBB+fn5Vhqxms9lh4h4J17npF4XDhw9HEATS0tKIjY2tsl0ul6NUKjl58iRHjhxh+fLlHDx40DZvcUarVq2YOnUqHTp04MMPPyQrK4u+ffvi6+vL/Pnz2b17N99++y0HDhyw7TN48GD8/Pz44IMPOHjwIPHx8WzcuJHevXvbks5369aN7OxsGjduTNu2benQoQONGjXi6tWrtXYQuVxO165dyc7Opl+/fgD069ePzMxMFApFlRRtFe1NT0/n448/Zu/evcydO9du+5AhQ0hJSWHhwoXs2bOHTz75xG776NGj+e2335g3bx7JycksWbKEQYMG2W5zgRuiAAASx0lEQVRA/v7+pKenk5SUVOXJLlE9N+0gwcHB9OrVy7acWhl3d3c+/PBDcnJyiIuLY+3atURERFBWVmaXa1AMuVzORx99hNFo5K233sLHx4f58+dTWFjI5MmTWbdund2PLzg4mIULF6LVannxxReZNWsWPXr04KOPPrLV6dGjB25ubkRHRyOTyZDJZERHR+Pm5kZUVFStz0W5c0VHRwMQFhZG48aN6dSpk93cqiLleQ+/++47Jk+eTPPmze22T548mQEDBvD111/z6quv2pZp3dysl+7111/nb3/7GytXrmTs2LF8/fXXDBs2zLZwMXbsWDQaDZMmTaoy75OoHllaWppwOwLHmc1mCgsLq81FWFMEQUCtVtutYFWmsLAQpVKJj49PnRzzVlJaWopcLq/ynsJisaBWq3F3d0epVLJz505mzpzJggULGDRokK2e2WxGpVJVyXcC1rmK2WwWnctIOCY9Pb32cxBXkcvldeYcYF1mduYcwF21rCnmxD/++COLFi3imWeewd3dne+//54mTZpUWTKWy+W2IWRlxJ5eEtVz2xxEonYMGzaM3NxcEhMTMRqNdOvWjalTp9ZJAlSJ6rltQywJibsNKTavhEQ1SA4iIeEEyUEkJJwgOYiEhBMkB5GQcILkIBISTpAcRELCCZKDSEg4QXIQCQknSA4iIeEEyUEkJJzgsoMUFRUxb948Fi9eXGVbecC3ip/I1gSdTkebNm1Yv36903plZWW0adOG+Ph4l9q/GZKSkkhISKjzdiPnFJBRYMZkho+3laIzOP9E9pt9Ov75c0md21GZY2lGNv5eVm29fvPUnMmu/qO3ux2XHUSj0bBkyRLmz5/P+fPn7bYtXryYJUuWsGfPnjoz8K/Gw8MDDw+PW9a+TAY+HjJkstq3seaonvkJ2jqxx10hw0NxE8YAY5cWcfn6vRE0sNZDLF9fX37++Wfb32q1mqSkpCrfHly6dIn4+Hh+/fVX2yefKpWKbdu2kZGRwZo1azAajXb7nDlzhm3bttkCMBw9epQ1a9Y4jNX722+/sWrVKg4ePGj7Xvvw4cOcPn0asH4stH37diwWi63+yZMnyczMJCEhAZVKxbp169ixY4etTkWCg4PtvrNISUlh1apV7Nmzx2F9sAab+Pnnn21RJp0hk0GrhgoUbtYfZZlJYMvJMjallHG92MKe8/YxfE9fM7H6Nz2nr1nv3qeyTCSnmziVZeLQZSNpKjOnsm7c2Y+mGlFrredFaxDYde5Gexdyzfx4RM/hKzfOv7+XjAcCb/ws0lRm4o/qOZpm5EKumdT8Gz98gwm2nTLw0/EyivUCZgtsP20gLd9M0nkDWYWOz8/dRK0dpH///nYxsX755RcaNmxoi/YH8MMPPzB8+HASExNZsGABY8aMwWg0cv78eaZPn86oUaP4+OOP7QI5nzp1inHjxnH69Gm8vLxYvnw548ePZ/369bz00kt2NsyZM4eJEyeSkJDA1KlTmTFjBgBbtmyxBYjYuXMn06ZNs8UJnjFjBseOHePQoUNMmzaNuLg4NmzYwPTp0/n000+r9HPz5s1s3LgRgNWrV/OPf/yD0tJSFi9e7DCk5++//864cePIyckhJSWF2NhYuyiKlTGZBWasKUZvEhAEeGVlMWuPlXEpz8zk74v59y+ltroHLhlZcVBPdpGFF5ZrOHnVREGphSKdhdIygbxiC3kaC7M3l/7ZNkxdXcwvKdYh0+ErRlYdtt6kNv5exow1xRTpBT7fpbU9gZLOGfjhiLXOH1dNPLu4iNR8M2uPlfHSCg2JZ25cqw83l3Iyy0TC6TJeWanBIsDVAjNmC+RprDbd7dTaQaKjo9HpdOzduxewBo2LjY21i4nl4+PDp59+yuLFi/nPf/7DuXPnuHjxom37e++9R3Jysu1ruszMTF555RX69evHjBkzMJvNLFq0iPHjx7NmzRq7yB2XL19m5cqVzJ07l2+//ZbFixezdetWDh8+TI8ePTh58iQGg4GkpCQ8PT3ZvXs3GRkZ5Ofn277Gs1gsLFq0iFWrVjFo0CD279/vtM/79+9nzJgxvPzyy3zxxRcO41wBfPLJJ0yaNIlZs2bh4eHBqVOnanROj6QayVKbWRrnz4wB3rz1hLfd9taN5Hw80pc3BngT86g7h68Y6dXanYiWSkIbyRka7kHn5koyC8wU6QSOphlpFiS3PTWOphrp+YgSoxn+s7WUT8f4Mam3F1+N82fVYX2VedDSfTom9PDinUE+fDzSl07N7b+ve72fF2894c2C0X78cdVEmUlgQk8vvN1lDOvkQetGd39klZuKrBgTE8OGDRu4cOEC586dY/jw4XZ3y8cee4yEhASefvppZs6cCVi/vS4nLCzMrs3FixejVquZMmUKMpmMvLw8iouL6dq1K4DdUKd8CFUeKKFz5874+Pjwxx9/0LVrV0wmEydOnODAgQO8+uqrtvhU9evXt4t71ahRIwDq1auHVut8HP/iiy+ybNkyxo8fz5YtW5gwYUKVOm3btiUpKYlx48YxcuRIsrKynD5BKpJ63Uz7BxTI/7wqfl72c4EGfjcul5+nDJ2x6h1aIYfHQ5T8lmpk5zkDr/TyoqDEglor8Fuqid6h7lxVmynWC3y0tZSXVmh4Y00xFgGuFdkPia5cN9PxwRtO4e/p2B4vdxluMusQ7l6j1p/cGo1GRowYwYsvvoiXlxddu3aladOmdvOJN998Ex8fH5YtW0ZBQQFDhw512manTp0oLS3lH//4BytWrLAFia4cqRGs4WzAugIWGBiI0WikrKwMDw8P/P396dChA1999RXBwcE8//zzLFq0iF9//ZUePXogq+WMuHPnziQmJpKSksKPP/7Ixo0bq6yoLVq0iJycHJYuXYqnpycjR46scfveHjIMdbAw1Ku1O4cuGzlyxcjMAd6kZLrzy+9laA0CIfXl5GosKOQy/vW0D24VTkVDP/v7pbeHDMO9MdeuNbV+ggiCwGOPPUaTJk3YtGkTI0aMqFLn2rVrBAQEIJPJ2LZtG4DTlAixsbH83//9H6dOnWLp0qUEBgbSunVrfvjhB7Kzs9m0aZOtbufOnQkICGDJkiUUFhaydOlSBEGgV69egDW0z6FDh4iOjsbLy4tu3bqxb9++Wse9AmsMsP379xMeHs706dM5e/ZslfhemZmZNGvWDE9PTy5dukRGRkaN00B0bq4gOd3IVbX1Tn4srWbeonCzv3v3fETJ5j/KeKiBHC93Gf3auvP1Xi09HrFGO2nk70bLYDeOXDHyYD05Df3c2H/RiNzN/sYR2VLJhuN6BAF0BsG2MFAdSjnoHOcHuuu4qReFMpmM2NhY/Pz8bMHSKjJz5kx27txJz549OXfuHEC1SXFatWrFtGnT+Oyzz0hJSeHDDz8kLS2Nvn37snXrVttTxd/fnwULFrBr1y66du3Kt99+y4cffmiLYVsegLrcrv79+yOTyW4q7tWbb77JBx98wNChQxkxYgTvvPMOCoX9Q/iFF14gPj6e6Oho3n33XZo0aUJ6enqN2m8WJGdKX29GfVXIEwsK2XexZr+yrg8r2XvRyKvfFwPWoU+LIDn92roDEPagAk+ljF6t3W37fDzKjx+O6Hn680JiFhRyvcRiG9qVM7GXF9dLBPrNUzPiyyJk1Gw5um8bd17/UcOWk9W/T7nTueVBG8qHPjcTekYQBEpKSkQjeRQWFuLn53fbwm1qNBp8fHxEj2exWCgpKbENA11qWyfgqQSLAPsvGll2QMcPE6uPZ6U3ClgE8HZ3bfio0Ql4KHH47sNgsrZpNIOvh4wJKzQM7+TB0x2rfy9UqBXw85RVcbq7ifT0dCmqyV/Bf3drKSitOqG1CLDtVBlNA+V4KuBinpnwZkqaBf01v7Kraguns0y0qO9Gsc66jPxEew8UNTSnoZ8br/T2qr7iHYrkIHcgxXqBfRcN5JcIdH1I+ZcvlV7INZOcZsTLXUZMO3eXn1B3M7c1sqJEzfDzlPFkh1snbXGV1o3kf7mT/pXcxSNECYlbzx3hICVlAu3+pcJgqvsXTR9vK60zIZ/E/ccd4SASEncqLs9BivUCKZkmWtaXc/iKgcb+crq3UnIm28TvGSYebiin60M3wu9nFpg5kmrC2x2i27jjqZShMwgcumIkpL6cExkmYtq52x3jbLaJkjKBiJZKtp820DdUibtCRplJYM95IzHt3LlWaCG/xIJcBqeumWj/gIJ2TavvTmq+mWNpRvy93Ihu445Sbl3qPJll4uEGcvZfMhLsI6NPqLttzT+zwMzhP1+qNfJ3wyzAIw3l7LtopMMDCgK9rRUTzxro/rASL3errQcvGcnVWHispZJWDW+M4y/mmTmebqRNYwUKuVXC0SzIuv30NRN/ZJp4pJGcx1oqq9gvcXtx+QlyrdDM9DXFzP21lOwiC7N/KeHl7zQs2q1DrbXw7k8ltg9udp0zMGllMUU6C7vPGRm/TIMggKrUqmB9a20JZyt9dHMiw8Sr3xfj6yFDEGDGmmJK/lSFFusF3lxrfRmWnG7k1e81LN6n46rawoRvNSSccf5ibePvZUz70dre5pQyXv3eKkXPVJuZGV/MrE0l5GksfLxNyzf7rfqpC7lmRn9dxIVcM5v/KOOF5Rq2/vkCbM6vpaSrbrwlf/enElSlAjqDwPNLNey5YKRYL/DCcg1HU60SnEOXjcR9U0RmgYXlB3RMWlnMwcvWbT/+puf99SUU6QX+b7uWr/bUTMMlceuo1SqWuxwWPuuHUg4h9eUsSNCy4416yN0gwMuNfRcMDA33wFMp49Nn/XikoRyLAFFzC8gqNAPWO+6SOH/qectsDnAqy8wba4pZOMaPtk0UVJeHskWwnIVjrC8PQxvLWbJXx4BH3UXrB3rJ+Pw5P1oEy3nucYHHPihAo7MeRCaDec/44eMho0WwGz8dL+Olnl4s269jTKQnU/tZlbXvr6/+q75Sg8DfunvaVqP0RoGEswYiQpR8vUfHtP7ejI6wJsp5YbnGVmdBgpYNUwJ5INCNEZ09GLSwkIk9ve7ql213O7VyEA+FDOWfIwZ/TxnBPm62i+jjIUP7p16xfVMFXyZpOXXNRJkR9CYBnRG8lKCQy6jnbb+mPm11MV0fUhLevGZm+Xjc2L9TcyWzNpU6qQ0dHlTw3906zueYMJhAwGoTWN9Al7fn6yGzSb+v5JsZ1P6G0/l5Vv8eoL6vG0q5jIkrNJSUCeRqLPT8Uwd1Jd9MeLOqCtkr+WYMJoH/2XjDAQ0m68u5JgGSh/xV3NL3IHO3lNLQ343lfwtAIYdeH6ud1p8+wJtFu7UknDHYngQ1Fd5qDQJeSueV/7GhlE7NFbz/ZAByN+j47+pz9nm7iytaZVidrDKnskx8sr2Ub18MoEmAG0v26riqNt9oz4Hmz9tdhqdSxqwh9pmmGvhKzvFXckvPfmaBmRbBchRyOJ5uQqOzYDKLj5ueDnNnwWg/Zv9SylW1GZnMejcu/7654qek1vYttuHZhuN6Ordw7u8ZBWZCGsiRu8G+i0bMFutXd86IbKlkw4kyzBbr57B/XL1hQwM/N678aduFXLNtmTpTbcHXw42Gfm7ojQKHrxgxmsvbU7D+hHUOo9EJXMi1bmhWT049bzdSMk08WE9Ofd9yha1z+yRuLbf0CTKpjzfv/lTMV0lamgdZL3pmgYVHm4pf9bAHFfytuycz4ktY9VIAU/p68foPxQR4ufFQA/s3ujIZvLhcQ2mZgEwGX45znpbstWhv/vlzCf+7RUabxgoCvGRkFpjxdTJsGt/NkzdWF9N/nhoPpQz/Ch8xvdTTk3d+KmHJXh2NAtzw/PMJ1jdUSfxRPX0/UaNwg3ZNFWT++QR5rZ83r/1gbc/LXYZCbn0Syd1g3mhf3llXwtd7dBTpBJ6P8rypYA4SN88t12KZLdbhT03G7mKUmawBASrqgH5JKeOXlDIWj/dHoxPsfrjOMJmt8w5fj5rVL39ymC3WOc/UH4vp+YjSNsk2mUFndNw/jU7Ax8Ne0aozCCjk1jmOv5eMkV8W8Xo/L3pXkKIX6QS8lNYIIxJ/HbXSYn2w2flE+HaRpjKTnm++5fbkFFk4kWkkJFhOqUHgWqGFet5uXMit3XGvXDdz+bqZ5kFy1FoLRTqBpHNG9l4wOqz/WEul3SKBxO3FZQf55+A7I+f4pTwzl/LMDLwNP57L1838lmrEXSEj5lH3m3oaApzOMnEi00SAl1Uhe7NxqCRuHZLcXUJCBCnLrYRENUgOIiHhBMlBJCScIDmIhIQTJAeRkHCC5CASEk6QHERCwglubm5uNQ6NKSFxv2A2m3Fzc8PN09PTLuK6hISENfGSp6cnboGBgWg0GjQajfQkkbjvMZvNaDQaiouLCQoKQiYIgmAymVCr1ej1etG0YhIS9wNubm54enoSFBSEXC7n/wF1srhlxTIbmgAAAABJRU5ErkJggg==",
175 175 "description": "Renders markdown/HTML using configurable pattern or function with applied attributes or timeseries values.",
176 176 "descriptor": {
... ... @@ -180,10 +180,10 @@
180 180 "resources": [],
181 181 "templateHtml": "<tb-markdown-widget \n [ctx]=\"ctx\">\n</tb-markdown-widget>",
182 182 "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",
183   - "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",
184   - "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",
  183 + "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",
  184 + "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",
185 185 "dataKeySettingsSchema": "{}\n",
186   - "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}"
  186 + "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}"
187 187 }
188 188 }
189 189 ]
... ...
... ... @@ -73,7 +73,7 @@
73 73 "templateHtml": "<tb-switch [ctx]='ctx'></tb-switch>",
74 74 "templateCss": "",
75 75 "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n",
76   - "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}",
  76 + "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}",
77 77 "dataKeySettingsSchema": "{}\n",
78 78 "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}"
79 79 }
... ... @@ -91,7 +91,7 @@
91 91 "templateHtml": "<tb-round-switch [ctx]='ctx'></tb-round-switch>",
92 92 "templateCss": "",
93 93 "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n",
94   - "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}",
  94 + "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}",
95 95 "dataKeySettingsSchema": "{}\n",
96 96 "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}"
97 97 }
... ... @@ -109,7 +109,7 @@
109 109 "templateHtml": "<tb-led-indicator [ctx]='ctx'></tb-led-indicator>",
110 110 "templateCss": "",
111 111 "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n",
112   - "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}",
  112 + "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}",
113 113 "dataKeySettingsSchema": "{}\n",
114 114 "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}"
115 115 }
... ... @@ -151,4 +151,4 @@
151 151 }
152 152 }
153 153 ]
154   -}
  154 +}
\ No newline at end of file
... ...
... ... @@ -19,8 +19,8 @@
19 19 "templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
20 20 "templateCss": "",
21 21 "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",
22   - "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}",
23   - "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}",
  22 + "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}",
  23 + "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}",
24 24 "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\":\"<form #addDeviceForm=\\\"ngForm\\\" [formGroup]=\\\"addDeviceFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Add device</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n type=\\\"button\\\">\\n <mat-icon class=\\\"material-icons\\\">close</mat-icon>\\n </button>\\n </mat-toolbar>\\n <mat-progress-bar color=\\\"warn\\\" mode=\\\"indeterminate\\\" *ngIf=\\\"isLoading$ | async\\\">\\n </mat-progress-bar>\\n <div style=\\\"height: 4px;\\\" *ngIf=\\\"!(isLoading$ | async)\\\"></div>\\n <div mat-dialog-content>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Device name</mat-label>\\n <input matInput formControlName=\\\"deviceName\\\" required>\\n <mat-error *ngIf=\\\"addDeviceFormGroup.get('deviceName').hasError('required')\\\">\\n Device name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"deviceType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'DEVICE'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"deviceLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || addDeviceForm.invalid || !addDeviceForm.dirty\\\">\\n Create\\n </button>\\n </div>\\n</form>\\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\":\"<form #editDeviceForm=\\\"ngForm\\\" [formGroup]=\\\"editDeviceFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Edit device</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n type=\\\"button\\\">\\n <mat-icon class=\\\"material-icons\\\">close</mat-icon>\\n </button>\\n </mat-toolbar>\\n <mat-progress-bar color=\\\"warn\\\" mode=\\\"indeterminate\\\" *ngIf=\\\"isLoading$ | async\\\">\\n </mat-progress-bar>\\n <div style=\\\"height: 4px;\\\" *ngIf=\\\"!(isLoading$ | async)\\\"></div>\\n <div mat-dialog-content>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Device name</mat-label>\\n <input matInput formControlName=\\\"deviceName\\\" required>\\n <mat-error *ngIf=\\\"editDeviceFormGroup.get('deviceName').hasError('required')\\\">\\n Device name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"deviceType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'DEVICE'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"deviceLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || editDeviceForm.invalid || !editDeviceForm.dirty\\\">\\n Update\\n </button>\\n </div>\\n</form>\\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\"}]}}"
25 25 }
26 26 },
... ... @@ -37,8 +37,8 @@
37 37 "templateHtml": "<tb-entities-table-widget \n [ctx]=\"ctx\">\n</tb-entities-table-widget>",
38 38 "templateCss": "",
39 39 "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",
40   - "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}",
41   - "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}",
  40 + "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}",
  41 + "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}",
42 42 "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\":\"<form #addAssetForm=\\\"ngForm\\\" [formGroup]=\\\"addAssetFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Add asset</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n type=\\\"button\\\">\\n <mat-icon class=\\\"material-icons\\\">close</mat-icon>\\n </button>\\n </mat-toolbar>\\n <mat-progress-bar color=\\\"warn\\\" mode=\\\"indeterminate\\\" *ngIf=\\\"isLoading$ | async\\\">\\n </mat-progress-bar>\\n <div style=\\\"height: 4px;\\\" *ngIf=\\\"!(isLoading$ | async)\\\"></div>\\n <div mat-dialog-content>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Asset name</mat-label>\\n <input matInput formControlName=\\\"assetName\\\" required>\\n <mat-error *ngIf=\\\"addAssetFormGroup.get('assetName').hasError('required')\\\">\\n Asset name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"assetType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'ASSET'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"assetLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n style=\\\"margin-right: 20px;\\\"\\n type=\\\"submit\\\"\\n [disabled]=\\\"(isLoading$ | async) || addAssetForm.invalid || !addAssetForm.dirty\\\">\\n Create\\n </button>\\n </div>\\n</form>\\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\":\"<form #editAssetForm=\\\"ngForm\\\" [formGroup]=\\\"editAssetFormGroup\\\"\\n (ngSubmit)=\\\"save()\\\" style=\\\"width: 480px;\\\">\\n <mat-toolbar fxLayout=\\\"row\\\" color=\\\"primary\\\">\\n <h2>Edit asset</h2>\\n <span fxFlex></span>\\n <button mat-button mat-icon-button\\n (click)=\\\"cancel()\\\"\\n type=\\\"button\\\">\\n <mat-icon class=\\\"material-icons\\\">close</mat-icon>\\n </button>\\n </mat-toolbar>\\n <mat-progress-bar color=\\\"warn\\\" mode=\\\"indeterminate\\\" *ngIf=\\\"isLoading$ | async\\\">\\n </mat-progress-bar>\\n <div style=\\\"height: 4px;\\\" *ngIf=\\\"!(isLoading$ | async)\\\"></div>\\n <div mat-dialog-content>\\n <div class=\\\"mat-padding\\\" fxLayout=\\\"column\\\">\\n <mat-form-field class=\\\"mat-block\\\">\\n <mat-label>Asset name</mat-label>\\n <input matInput formControlName=\\\"assetName\\\" required>\\n <mat-error *ngIf=\\\"editAssetFormGroup.get('assetName').hasError('required')\\\">\\n Asset name is required.\\n </mat-error>\\n </mat-form-field>\\n <div fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <tb-entity-subtype-autocomplete\\n fxFlex=\\\"50\\\"\\n formControlName=\\\"assetType\\\"\\n [required]=\\\"true\\\"\\n [entityType]=\\\"'ASSET'\\\"\\n ></tb-entity-subtype-autocomplete>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Label</mat-label>\\n <input matInput formControlName=\\\"assetLabel\\\">\\n </mat-form-field>\\n </div>\\n <div formGroupName=\\\"attributes\\\" fxLayout=\\\"row\\\" fxLayoutGap=\\\"8px\\\">\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Latitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"latitude\\\">\\n </mat-form-field>\\n <mat-form-field fxFlex=\\\"50\\\" class=\\\"mat-block\\\">\\n <mat-label>Longitude</mat-label>\\n <input type=\\\"number\\\" step=\\\"any\\\" matInput formControlName=\\\"longitude\\\">\\n </mat-form-field>\\n </div>\\n </div> \\n </div>\\n <div mat-dialog-actions fxLayout=\\\"row\\\">\\n <span fxFlex></span>\\n <button mat-button color=\\\"primary\\\"\\n type=\\\"button\\\"\\n [disabled]=\\\"(isLoading$ | async)\\\"\\n (click)=\\\"cancel()\\\" cdkFocusInitial>\\n Cancel\\n </button>\\n <button mat-button mat-raised-button color=\\\"primary\\\"\\n type=\\\"submit\\\"\\n style=\\\"margin-right: 20px;\\\"\\n [disabled]=\\\"(isLoading$ | async) || editAssetForm.invalid || !editAssetForm.dirty\\\">\\n Update\\n </button>\\n </div>\\n</form>\\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\"}]}}"
43 43 }
44 44 }
... ...
... ... @@ -19,7 +19,7 @@
19 19 "templateHtml": "<fieldset class=\"gpio-panel\" style=\"height: 100%;\">\n <section class=\"gpio-row\" fxLayout=\"row\" *ngFor=\"let row of rows\" \n [ngStyle]=\"{'height': prefferedRowHeight+'px'}\">\n <section fxFlex fxLayout=\"row\" *ngFor=\"let cell of row; let $index = index\">\n <section fxLayout=\"row\" fxFlex *ngIf=\"cell\" fxLayoutAlign=\"{{$index===0 ? 'end center' : 'start center'}}\">\n <span class=\"gpio-left-label\" [fxShow]=\"$index===0\">{{ cell.label }}</span>\n <section fxLayout=\"row\" class=\"switch-panel\" fxLayoutAlign=\"start center\" [ngClass]=\"$index===0 ? 'col-0' : 'col-1'\"\n [ngStyle]=\"{'height': prefferedRowHeight+'px', 'backgroundColor': switchPanelBackgroundColor }\">\n <span class=\"pin\" [fxShow]=\"$index===0\">{{cell.pin}}</span>\n <span fxFlex [fxShow]=\"$index===1\"></span>\n <mat-slide-toggle\n [disabled]=\"!rpcEnabled || executingRpcRequest\"\n [checked]=\"cell.enabled\" \n (change)=\"gpioToggleChange($event, cell)\" \n (click)=\"gpioClick($event, cell)\">\n </mat-slide-toggle>\n <span fxFlex [fxShow]=\"$index===0\"></span>\n <span class=\"pin\" [fxShow]=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" [fxShow]=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section fxLayout=\"row\" fxFlex *ngIf=\"!cell\">\n <span fxFlex [fxShow]=\"$index===0\"></span>\n <span class=\"switch-panel\"\n [ngStyle]=\"{'height': prefferedRowHeight+'px', 'backgroundColor': switchPanelBackgroundColor }\"></span>\n <span fxFlex [fxShow]=\"$index===1\"></span>\n </section>\n </section>\n </section> \n <span class=\"error\" style=\"position: absolute; bottom: 5px;\" [fxShow]=\"rpcErrorText\">{{rpcErrorText}}</span>\n <mat-progress-bar [fxShow]=\"executingRpcRequest\" style=\"position: absolute; bottom: 0;\" mode=\"indeterminate\"></mat-progress-bar>\n</fieldset>",
20 20 "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}",
21 21 "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",
22   - "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}",
  22 + "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}",
23 23 "dataKeySettingsSchema": "{}\n",
24 24 "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\"}"
25 25 }
... ... @@ -73,7 +73,7 @@
73 73 "templateHtml": "<fieldset class=\"gpio-panel\" style=\"height: 100%;\">\n <section class=\"gpio-row\" fxLayout=\"row\" *ngFor=\"let row of rows\" \n [ngStyle]=\"{'height': prefferedRowHeight+'px'}\">\n <section fxFlex fxLayout=\"row\" *ngFor=\"let cell of row; let $index = index\">\n <section fxLayout=\"row\" fxFlex *ngIf=\"cell\" fxLayoutAlign=\"{{$index===0 ? 'end center' : 'start center'}}\">\n <span class=\"gpio-left-label\" [fxShow]=\"$index===0\">{{ cell.label }}</span>\n <section fxLayout=\"row\" class=\"switch-panel\" fxLayoutAlign=\"start center\" [ngClass]=\"$index===0 ? 'col-0' : 'col-1'\"\n [ngStyle]=\"{'height': prefferedRowHeight+'px', 'backgroundColor': switchPanelBackgroundColor }\">\n <span class=\"pin\" [fxShow]=\"$index===0\">{{cell.pin}}</span>\n <span fxFlex [fxShow]=\"$index===1\"></span>\n <mat-slide-toggle\n [disabled]=\"!rpcEnabled || executingRpcRequest\"\n [checked]=\"cell.enabled\" \n (change)=\"gpioToggleChange($event, cell)\" \n (click)=\"gpioClick($event, cell)\">\n </mat-slide-toggle>\n <span fxFlex [fxShow]=\"$index===0\"></span>\n <span class=\"pin\" [fxShow]=\"$index===1\">{{cell.pin}}</span>\n </section>\n <span class=\"gpio-right-label\" [fxShow]=\"$index===1\">{{ cell.label }}</span>\n </section>\n <section fxLayout=\"row\" fxFlex *ngIf=\"!cell\">\n <span fxFlex [fxShow]=\"$index===0\"></span>\n <span class=\"switch-panel\"\n [ngStyle]=\"{'height': prefferedRowHeight+'px', 'backgroundColor': switchPanelBackgroundColor }\"></span>\n <span fxFlex [fxShow]=\"$index===1\"></span>\n </section>\n </section>\n </section> \n <span class=\"error\" style=\"position: absolute; bottom: 5px;\" [fxShow]=\"rpcErrorText\">{{rpcErrorText}}</span>\n <mat-progress-bar [fxShow]=\"executingRpcRequest\" style=\"position: absolute; bottom: 0;\" mode=\"indeterminate\"></mat-progress-bar>\n</fieldset>",
74 74 "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}",
75 75 "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",
76   - "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}",
  76 + "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}",
77 77 "dataKeySettingsSchema": "{}\n",
78 78 "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\"}"
79 79 }
... ...
... ... @@ -38,7 +38,7 @@
38 38 "templateCss": ".tb-toast {\n min-width: 0;\n font-size: 14px !important;\n}",
39 39 "controllerScript": "self.onInit = function() {\r\n}\r\n\r\nself.onDataUpdated = function() {\r\n self.ctx.$scope.multipleInputWidget.onDataUpdated();\r\n}\r\n",
40 40 "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}",
41   - "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",
  41 + "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",
42 42 "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\":{}}"
43 43 }
44 44 },
... ...
... ... @@ -59,7 +59,8 @@ import org.thingsboard.server.dao.edge.EdgeEventService;
59 59 import org.thingsboard.server.dao.edge.EdgeService;
60 60 import org.thingsboard.server.dao.entityview.EntityViewService;
61 61 import org.thingsboard.server.dao.event.EventService;
62   -import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
  62 +import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor;
  63 +import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor;
63 64 import org.thingsboard.server.dao.ota.OtaPackageService;
64 65 import org.thingsboard.server.dao.relation.RelationService;
65 66 import org.thingsboard.server.dao.resource.ResourceService;
... ... @@ -85,7 +86,6 @@ import org.thingsboard.server.cluster.TbClusterService;
85 86 import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
86 87 import org.thingsboard.server.service.rpc.TbRpcService;
87 88 import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
88   -import org.thingsboard.server.service.script.JsExecutorService;
89 89 import org.thingsboard.server.service.script.JsInvokeService;
90 90 import org.thingsboard.server.service.session.DeviceSessionCacheService;
91 91 import org.thingsboard.server.service.sms.SmsExecutorService;
... ... @@ -421,7 +421,11 @@ public class ActorSystemContext {
421 421
422 422 @Autowired(required = false)
423 423 @Getter
424   - private CassandraBufferedRateExecutor cassandraBufferedRateExecutor;
  424 + private CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor;
  425 +
  426 + @Autowired(required = false)
  427 + @Getter
  428 + private CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor;
425 429
426 430 @Autowired(required = false)
427 431 @Getter
... ...
... ... @@ -244,7 +244,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
244 244 rpc.setExpirationTime(request.getExpirationTime());
245 245 rpc.setRequest(JacksonUtil.valueToTree(request));
246 246 rpc.setStatus(status);
247   - rpc.setAdditionalInfo(JacksonUtil.valueToTree(request.getAdditionalInfo()));
  247 + rpc.setAdditionalInfo(JacksonUtil.toJsonNode(request.getAdditionalInfo()));
248 248 return systemContext.getTbRpcService().save(tenantId, rpc);
249 249 }
250 250
... ... @@ -731,6 +731,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
731 731 }
732 732
733 733 private void notifyTransportAboutClosedSessionMaxSessionsLimit(UUID sessionId, SessionInfoMetaData sessionMd) {
  734 + log.debug("remove eldest session (max concurrent sessions limit reached per device) sessionId [{}] sessionMd [{}]", sessionId, sessionMd);
734 735 notifyTransportAboutClosedSession(sessionId, sessionMd, "max concurrent sessions limit reached per device!");
735 736 }
736 737
... ...
... ... @@ -190,13 +190,13 @@ class DefaultTbContext implements TbContext {
190 190 @Override
191 191 public void enqueueForTellNext(TbMsg tbMsg, String queueName, String relationType, Runnable onSuccess, Consumer<Throwable> onFailure) {
192 192 TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName);
193   - enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure);
  193 + enqueueForTellNext(tpi, queueName, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure);
194 194 }
195 195
196 196 @Override
197 197 public void enqueueForTellNext(TbMsg tbMsg, String queueName, Set<String> relationTypes, Runnable onSuccess, Consumer<Throwable> onFailure) {
198 198 TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName);
199   - enqueueForTellNext(tpi, tbMsg, relationTypes, null, onSuccess, onFailure);
  199 + enqueueForTellNext(tpi, queueName, tbMsg, relationTypes, null, onSuccess, onFailure);
200 200 }
201 201
202 202 private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) {
... ... @@ -211,9 +211,13 @@ class DefaultTbContext implements TbContext {
211 211 }
212 212
213 213 private void enqueueForTellNext(TopicPartitionInfo tpi, TbMsg source, Set<String> relationTypes, String failureMessage, Runnable onSuccess, Consumer<Throwable> onFailure) {
  214 + enqueueForTellNext(tpi, source.getQueueName(), source, relationTypes, failureMessage, onSuccess, onFailure);
  215 + }
  216 +
  217 + private void enqueueForTellNext(TopicPartitionInfo tpi, String queueName, TbMsg source, Set<String> relationTypes, String failureMessage, Runnable onSuccess, Consumer<Throwable> onFailure) {
214 218 RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId();
215 219 RuleNodeId ruleNodeId = nodeCtx.getSelf().getId();
216   - TbMsg tbMsg = TbMsg.newMsg(source, ruleChainId, ruleNodeId);
  220 + TbMsg tbMsg = TbMsg.newMsg(source, queueName, ruleChainId, ruleNodeId);
217 221 TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder()
218 222 .setTenantIdMSB(getTenantId().getId().getMostSignificantBits())
219 223 .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits())
... ... @@ -553,8 +557,13 @@ class DefaultTbContext implements TbContext {
553 557 }
554 558
555 559 @Override
556   - public TbResultSetFuture submitCassandraTask(CassandraStatementTask task) {
557   - return mainCtx.getCassandraBufferedRateExecutor().submit(task);
  560 + public TbResultSetFuture submitCassandraReadTask(CassandraStatementTask task) {
  561 + return mainCtx.getCassandraBufferedRateReadExecutor().submit(task);
  562 + }
  563 +
  564 + @Override
  565 + public TbResultSetFuture submitCassandraWriteTask(CassandraStatementTask task) {
  566 + return mainCtx.getCassandraBufferedRateWriteExecutor().submit(task);
558 567 }
559 568
560 569 @Override
... ...
... ... @@ -60,7 +60,7 @@ public abstract class AbstractRpcController extends BaseController {
60 60 protected TbCoreDeviceRpcService deviceRpcService;
61 61
62 62 @Autowired
63   - private AccessValidator accessValidator;
  63 + protected AccessValidator accessValidator;
64 64
65 65 @Value("${server.rest.server_side_rpc.min_timeout:5000}")
66 66 protected long minTimeout;
... ...
... ... @@ -16,6 +16,8 @@
16 16 package org.thingsboard.server.controller;
17 17
18 18 import com.fasterxml.jackson.databind.node.ObjectNode;
  19 +import io.swagger.annotations.ApiOperation;
  20 +import io.swagger.annotations.ApiParam;
19 21 import org.springframework.beans.factory.annotation.Autowired;
20 22 import org.springframework.security.access.prepost.PreAuthorize;
21 23 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -39,6 +41,8 @@ import org.thingsboard.server.service.security.permission.Resource;
39 41 import org.thingsboard.server.service.security.system.SystemSecurityService;
40 42 import org.thingsboard.server.service.update.UpdateService;
41 43
  44 +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
  45 +
42 46 @RestController
43 47 @TbCoreComponent
44 48 @RequestMapping("/api/admin")
... ... @@ -59,10 +63,14 @@ public class AdminController extends BaseController {
59 63 @Autowired
60 64 private UpdateService updateService;
61 65
  66 + @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
  67 + notes = "Get the Administration Settings object using specified string key. Referencing non-existing key will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
62 68 @PreAuthorize("hasAuthority('SYS_ADMIN')")
63 69 @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET)
64 70 @ResponseBody
65   - public AdminSettings getAdminSettings(@PathVariable("key") String key) throws ThingsboardException {
  71 + public AdminSettings getAdminSettings(
  72 + @ApiParam(value = "A string value of the key (e.g. 'general' or 'mail').")
  73 + @PathVariable("key") String key) throws ThingsboardException {
66 74 try {
67 75 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
68 76 AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key));
... ... @@ -75,10 +83,17 @@ public class AdminController extends BaseController {
75 83 }
76 84 }
77 85
  86 +
  87 + @ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)",
  88 + notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " +
  89 + "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
  90 + "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
78 91 @PreAuthorize("hasAuthority('SYS_ADMIN')")
79 92 @RequestMapping(value = "/settings", method = RequestMethod.POST)
80 93 @ResponseBody
81   - public AdminSettings saveAdminSettings(@RequestBody AdminSettings adminSettings) throws ThingsboardException {
  94 + public AdminSettings saveAdminSettings(
  95 + @ApiParam(value = "A JSON value representing the Administration Settings.")
  96 + @RequestBody AdminSettings adminSettings) throws ThingsboardException {
82 97 try {
83 98 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
84 99 adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings));
... ... @@ -94,6 +109,8 @@ public class AdminController extends BaseController {
94 109 }
95 110 }
96 111
  112 + @ApiOperation(value = "Get the Security Settings object",
  113 + notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
97 114 @PreAuthorize("hasAuthority('SYS_ADMIN')")
98 115 @RequestMapping(value = "/securitySettings", method = RequestMethod.GET)
99 116 @ResponseBody
... ... @@ -106,10 +123,14 @@ public class AdminController extends BaseController {
106 123 }
107 124 }
108 125
  126 + @ApiOperation(value = "Update Security Settings (saveSecuritySettings)",
  127 + notes = "Updates the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
109 128 @PreAuthorize("hasAuthority('SYS_ADMIN')")
110 129 @RequestMapping(value = "/securitySettings", method = RequestMethod.POST)
111 130 @ResponseBody
112   - public SecuritySettings saveSecuritySettings(@RequestBody SecuritySettings securitySettings) throws ThingsboardException {
  131 + public SecuritySettings saveSecuritySettings(
  132 + @ApiParam(value = "A JSON value representing the Security Settings.")
  133 + @RequestBody SecuritySettings securitySettings) throws ThingsboardException {
113 134 try {
114 135 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.WRITE);
115 136 securitySettings = checkNotNull(systemSecurityService.saveSecuritySettings(TenantId.SYS_TENANT_ID, securitySettings));
... ... @@ -119,14 +140,19 @@ public class AdminController extends BaseController {
119 140 }
120 141 }
121 142
  143 + @ApiOperation(value = "Send test email (sendTestMail)",
  144 + notes = "Attempts to send test email to the System Administrator User using Mail Settings provided as a parameter. " +
  145 + "You may change the 'To' email in the user profile of the System Administrator. " + SYSTEM_AUTHORITY_PARAGRAPH)
122 146 @PreAuthorize("hasAuthority('SYS_ADMIN')")
123 147 @RequestMapping(value = "/settings/testMail", method = RequestMethod.POST)
124   - public void sendTestMail(@RequestBody AdminSettings adminSettings) throws ThingsboardException {
  148 + public void sendTestMail(
  149 + @ApiParam(value = "A JSON value representing the Mail Settings.")
  150 + @RequestBody AdminSettings adminSettings) throws ThingsboardException {
125 151 try {
126 152 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
127 153 adminSettings = checkNotNull(adminSettings);
128 154 if (adminSettings.getKey().equals("mail")) {
129   - if(!adminSettings.getJsonValue().has("password")) {
  155 + if (!adminSettings.getJsonValue().has("password")) {
130 156 AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"));
131 157 ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText());
132 158 }
... ... @@ -138,9 +164,14 @@ public class AdminController extends BaseController {
138 164 }
139 165 }
140 166
  167 + @ApiOperation(value = "Send test sms (sendTestMail)",
  168 + notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. "
  169 + + SYSTEM_AUTHORITY_PARAGRAPH)
141 170 @PreAuthorize("hasAuthority('SYS_ADMIN')")
142 171 @RequestMapping(value = "/settings/testSms", method = RequestMethod.POST)
143   - public void sendTestSms(@RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
  172 + public void sendTestSms(
  173 + @ApiParam(value = "A JSON value representing the Test SMS request.")
  174 + @RequestBody TestSmsRequest testSmsRequest) throws ThingsboardException {
144 175 try {
145 176 accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ);
146 177 smsService.sendTestSms(testSmsRequest);
... ... @@ -149,6 +180,9 @@ public class AdminController extends BaseController {
149 180 }
150 181 }
151 182
  183 + @ApiOperation(value = "Check for new Platform Releases (checkUpdates)",
  184 + notes = "Check notifications about new platform releases. "
  185 + + SYSTEM_AUTHORITY_PARAGRAPH)
152 186 @PreAuthorize("hasAuthority('SYS_ADMIN')")
153 187 @RequestMapping(value = "/updates", method = RequestMethod.GET)
154 188 @ResponseBody
... ...
... ... @@ -15,8 +15,11 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import io.swagger.annotations.ApiOperation;
  19 +import io.swagger.annotations.ApiParam;
18 20 import org.apache.commons.lang3.StringUtils;
19 21 import org.springframework.http.HttpStatus;
  22 +import org.springframework.http.MediaType;
20 23 import org.springframework.security.access.prepost.PreAuthorize;
21 24 import org.springframework.web.bind.annotation.PathVariable;
22 25 import org.springframework.web.bind.annotation.RequestBody;
... ... @@ -49,17 +52,46 @@ import org.thingsboard.server.service.security.permission.Resource;
49 52
50 53 import java.util.List;
51 54
  55 +import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
  56 +import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION;
  57 +import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES;
  58 +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID;
  59 +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
  60 +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE;
  61 +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
  62 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  63 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  64 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  65 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  66 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  67 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  68 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  69 +
52 70 @RestController
53 71 @TbCoreComponent
54 72 @RequestMapping("/api")
55 73 public class AlarmController extends BaseController {
56 74
57 75 public static final String ALARM_ID = "alarmId";
  76 + 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. " +
  77 + "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. ";
  78 + private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value";
  79 + private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK";
  80 + private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value";
  81 + private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK";
  82 + private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on of next alarm fields: type, severity or status";
  83 + 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'.";
  84 + 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'.";
  85 + private static final String ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION = "A boolean value to specify if the alarm originator name will be " +
  86 + "filled in the AlarmInfo object field: 'originatorName' or will returns as null.";
58 87
59   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  88 + @ApiOperation(value = "Get Alarm (getAlarmById)",
  89 + notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK, produces = MediaType.APPLICATION_JSON_VALUE)
  90 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
60 91 @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET)
61 92 @ResponseBody
62   - public Alarm getAlarmById(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
  93 + public Alarm getAlarmById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION)
  94 + @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
63 95 checkParameter(ALARM_ID, strAlarmId);
64 96 try {
65 97 AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
... ... @@ -69,10 +101,14 @@ public class AlarmController extends BaseController {
69 101 }
70 102 }
71 103
72   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  104 + @ApiOperation(value = "Get Alarm Info (getAlarmInfoById)",
  105 + notes = "Fetch the Alarm Info object based on the provided Alarm Id. " +
  106 + ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
  107 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
73 108 @RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET)
74 109 @ResponseBody
75   - public AlarmInfo getAlarmInfoById(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
  110 + public AlarmInfo getAlarmInfoById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION)
  111 + @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
76 112 checkParameter(ALARM_ID, strAlarmId);
77 113 try {
78 114 AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
... ... @@ -82,10 +118,20 @@ public class AlarmController extends BaseController {
82 118 }
83 119 }
84 120
85   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  121 + @ApiOperation(value = "Create or update Alarm (saveAlarm)",
  122 + notes = "Creates or Updates the Alarm. " +
  123 + "When creating alarm, platform generates Alarm Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
  124 + "The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " +
  125 + "Referencing non-existing Alarm Id will cause 'Not Found' error. " +
  126 + "\n\nPlatform also deduplicate the alarms based on the entity id of originator and alarm 'type'. " +
  127 + "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. " +
  128 + "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). " +
  129 + "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
  130 + , produces = MediaType.APPLICATION_JSON_VALUE)
  131 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
86 132 @RequestMapping(value = "/alarm", method = RequestMethod.POST)
87 133 @ResponseBody
88   - public Alarm saveAlarm(@RequestBody Alarm alarm) throws ThingsboardException {
  134 + public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException {
89 135 try {
90 136 alarm.setTenantId(getCurrentUser().getTenantId());
91 137
... ... @@ -106,10 +152,12 @@ public class AlarmController extends BaseController {
106 152 }
107 153 }
108 154
109   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  155 + @ApiOperation(value = "Delete Alarm (deleteAlarm)",
  156 + notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
  157 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
110 158 @RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE)
111 159 @ResponseBody
112   - public Boolean deleteAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
  160 + public Boolean deleteAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
113 161 checkParameter(ALARM_ID, strAlarmId);
114 162 try {
115 163 AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
... ... @@ -124,15 +172,19 @@ public class AlarmController extends BaseController {
124 172 sendAlarmDeleteNotificationMsg(getTenantId(), alarmId, relatedEdgeIds, alarm);
125 173
126 174 return alarmService.deleteAlarm(getTenantId(), alarmId);
127   - } catch (Exception e) {
  175 + } catch (Exception e) {
128 176 throw handleException(e);
129 177 }
130 178 }
131 179
132   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  180 + @ApiOperation(value = "Acknowledge Alarm (ackAlarm)",
  181 + notes = "Acknowledge the Alarm. " +
  182 + "Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " +
  183 + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
  184 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
133 185 @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST)
134 186 @ResponseStatus(value = HttpStatus.OK)
135   - public void ackAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
  187 + public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
136 188 checkParameter(ALARM_ID, strAlarmId);
137 189 try {
138 190 AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
... ... @@ -149,10 +201,14 @@ public class AlarmController extends BaseController {
149 201 }
150 202 }
151 203
152   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  204 + @ApiOperation(value = "Clear Alarm (clearAlarm)",
  205 + notes = "Clear the Alarm. " +
  206 + "Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " +
  207 + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
  208 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
153 209 @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST)
154 210 @ResponseStatus(value = HttpStatus.OK)
155   - public void clearAlarm(@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
  211 + public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
156 212 checkParameter(ALARM_ID, strAlarmId);
157 213 try {
158 214 AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
... ... @@ -169,21 +225,36 @@ public class AlarmController extends BaseController {
169 225 }
170 226 }
171 227
172   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  228 + @ApiOperation(value = "Get Alarms (getAlarms)",
  229 + notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
  230 + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
  231 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
173 232 @RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
174 233 @ResponseBody
175 234 public PageData<AlarmInfo> getAlarms(
176   - @PathVariable("entityType") String strEntityType,
177   - @PathVariable("entityId") String strEntityId,
  235 + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION)
  236 + @PathVariable(ENTITY_TYPE) String strEntityType,
  237 + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION)
  238 + @PathVariable(ENTITY_ID) String strEntityId,
  239 + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
178 240 @RequestParam(required = false) String searchStatus,
  241 + @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
179 242 @RequestParam(required = false) String status,
  243 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
180 244 @RequestParam int pageSize,
  245 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
181 246 @RequestParam int page,
  247 + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
182 248 @RequestParam(required = false) String textSearch,
  249 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
183 250 @RequestParam(required = false) String sortProperty,
  251 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
184 252 @RequestParam(required = false) String sortOrder,
  253 + @ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
185 254 @RequestParam(required = false) Long startTime,
  255 + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
186 256 @RequestParam(required = false) Long endTime,
  257 + @ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
187 258 @RequestParam(required = false) Boolean fetchOriginator
188 259 ) throws ThingsboardException {
189 260 checkParameter("EntityId", strEntityId);
... ... @@ -204,20 +275,35 @@ public class AlarmController extends BaseController {
204 275 throw handleException(e);
205 276 }
206 277 }
207   -
208   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  278 + @ApiOperation(value = "Get All Alarms (getAllAlarms)",
  279 + notes = "Returns a page of alarms that belongs to the current user owner. " +
  280 + "If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " +
  281 + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
  282 + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
  283 + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
  284 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
209 285 @RequestMapping(value = "/alarms", method = RequestMethod.GET)
210 286 @ResponseBody
211 287 public PageData<AlarmInfo> getAllAlarms(
  288 + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
212 289 @RequestParam(required = false) String searchStatus,
  290 + @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
213 291 @RequestParam(required = false) String status,
  292 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
214 293 @RequestParam int pageSize,
  294 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
215 295 @RequestParam int page,
  296 + @ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
216 297 @RequestParam(required = false) String textSearch,
  298 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
217 299 @RequestParam(required = false) String sortProperty,
  300 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
218 301 @RequestParam(required = false) String sortOrder,
  302 + @ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
219 303 @RequestParam(required = false) Long startTime,
  304 + @ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
220 305 @RequestParam(required = false) Long endTime,
  306 + @ApiParam(value = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
221 307 @RequestParam(required = false) Boolean fetchOriginator
222 308 ) throws ThingsboardException {
223 309 AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
... ... @@ -239,13 +325,21 @@ public class AlarmController extends BaseController {
239 325 }
240 326 }
241 327
242   - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
  328 + @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
  329 + 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). " +
  330 + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH
  331 + , produces = MediaType.APPLICATION_JSON_VALUE)
  332 + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
243 333 @RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET)
244 334 @ResponseBody
245 335 public AlarmSeverity getHighestAlarmSeverity(
246   - @PathVariable("entityType") String strEntityType,
247   - @PathVariable("entityId") String strEntityId,
  336 + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
  337 + @PathVariable(ENTITY_TYPE) String strEntityType,
  338 + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
  339 + @PathVariable(ENTITY_ID) String strEntityId,
  340 + @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
248 341 @RequestParam(required = false) String searchStatus,
  342 + @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES)
249 343 @RequestParam(required = false) String status
250 344 ) throws ThingsboardException {
251 345 checkParameter("EntityId", strEntityId);
... ...
... ... @@ -16,9 +16,12 @@
16 16 package org.thingsboard.server.controller;
17 17
18 18 import com.google.common.util.concurrent.ListenableFuture;
  19 +import io.swagger.annotations.ApiOperation;
  20 +import io.swagger.annotations.ApiParam;
19 21 import lombok.RequiredArgsConstructor;
20 22 import lombok.extern.slf4j.Slf4j;
21 23 import org.springframework.http.HttpStatus;
  24 +import org.springframework.http.MediaType;
22 25 import org.springframework.security.access.prepost.PreAuthorize;
23 26 import org.springframework.web.bind.annotation.PathVariable;
24 27 import org.springframework.web.bind.annotation.PostMapping;
... ... @@ -37,8 +40,8 @@ import org.thingsboard.server.common.data.asset.AssetInfo;
37 40 import org.thingsboard.server.common.data.asset.AssetSearchQuery;
38 41 import org.thingsboard.server.common.data.audit.ActionType;
39 42 import org.thingsboard.server.common.data.edge.Edge;
40   -import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
41 43 import org.thingsboard.server.common.data.edge.EdgeEventActionType;
  44 +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
42 45 import org.thingsboard.server.common.data.exception.ThingsboardException;
43 46 import org.thingsboard.server.common.data.id.AssetId;
44 47 import org.thingsboard.server.common.data.id.CustomerId;
... ... @@ -61,9 +64,28 @@ import java.util.ArrayList;
61 64 import java.util.List;
62 65 import java.util.stream.Collectors;
63 66
64   -import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
65   -
  67 +import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION;
  68 +import static org.thingsboard.server.controller.ControllerConstants.ASSET_INFO_DESCRIPTION;
  69 +import static org.thingsboard.server.controller.ControllerConstants.ASSET_NAME_DESCRIPTION;
  70 +import static org.thingsboard.server.controller.ControllerConstants.ASSET_SORT_PROPERTY_ALLOWABLE_VALUES;
  71 +import static org.thingsboard.server.controller.ControllerConstants.ASSET_TEXT_SEARCH_DESCRIPTION;
  72 +import static org.thingsboard.server.controller.ControllerConstants.ASSET_TYPE_DESCRIPTION;
  73 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
  74 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
  75 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
  76 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
  77 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
  78 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
  79 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  80 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  81 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  82 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  83 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  84 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  85 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  86 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
66 87 import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
  88 +import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
67 89
68 90 @RestController
69 91 @TbCoreComponent
... ... @@ -75,10 +97,16 @@ public class AssetController extends BaseController {
75 97
76 98 public static final String ASSET_ID = "assetId";
77 99
  100 + @ApiOperation(value = "Get Asset (getAssetById)",
  101 + notes = "Fetch the Asset object based on the provided Asset Id. " +
  102 + "If the user has the authority of 'Tenant Administrator', the server checks that the asset is owned by the same tenant. " +
  103 + "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
  104 + , produces = MediaType.APPLICATION_JSON_VALUE)
78 105 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
79 106 @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.GET)
80 107 @ResponseBody
81   - public Asset getAssetById(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  108 + public Asset getAssetById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION)
  109 + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
82 110 checkParameter(ASSET_ID, strAssetId);
83 111 try {
84 112 AssetId assetId = new AssetId(toUUID(strAssetId));
... ... @@ -88,10 +116,16 @@ public class AssetController extends BaseController {
88 116 }
89 117 }
90 118
  119 + @ApiOperation(value = "Get Asset Info (getAssetInfoById)",
  120 + notes = "Fetch the Asset Info object based on the provided Asset Id. " +
  121 + "If the user has the authority of 'Tenant Administrator', the server checks that the asset is owned by the same tenant. " +
  122 + "If the user has the authority of 'Customer User', the server checks that the asset is assigned to the same customer. "
  123 + + ASSET_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
91 124 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
92 125 @RequestMapping(value = "/asset/info/{assetId}", method = RequestMethod.GET)
93 126 @ResponseBody
94   - public AssetInfo getAssetInfoById(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  127 + public AssetInfo getAssetInfoById(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION)
  128 + @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
95 129 checkParameter(ASSET_ID, strAssetId);
96 130 try {
97 131 AssetId assetId = new AssetId(toUUID(strAssetId));
... ... @@ -101,10 +135,15 @@ public class AssetController extends BaseController {
101 135 }
102 136 }
103 137
  138 + @ApiOperation(value = "Create Or Update Asset (saveAsset)",
  139 + 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) " +
  140 + "The newly created Asset id will be present in the response. " +
  141 + "Specify existing Asset id to update the asset. " +
  142 + "Referencing non-existing Asset Id will cause 'Not Found' error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
104 143 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
105 144 @RequestMapping(value = "/asset", method = RequestMethod.POST)
106 145 @ResponseBody
107   - public Asset saveAsset(@RequestBody Asset asset) throws ThingsboardException {
  146 + public Asset saveAsset(@ApiParam(value = "A JSON value representing the asset.") @RequestBody Asset asset) throws ThingsboardException {
108 147 try {
109 148 if (TB_SERVICE_QUEUE.equals(asset.getType())) {
110 149 throw new ThingsboardException("Unable to save asset with type " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
... ... @@ -116,7 +155,7 @@ public class AssetController extends BaseController {
116 155
117 156 Asset savedAsset = checkNotNull(assetService.saveAsset(asset));
118 157
119   - onAssetCreatedOrUpdated(savedAsset, asset.getId() != null);
  158 + onAssetCreatedOrUpdated(savedAsset, asset.getId() != null, getCurrentUser());
120 159
121 160 return savedAsset;
122 161 } catch (Exception e) {
... ... @@ -126,9 +165,9 @@ public class AssetController extends BaseController {
126 165 }
127 166 }
128 167
129   - private void onAssetCreatedOrUpdated(Asset asset, boolean updated) {
  168 + private void onAssetCreatedOrUpdated(Asset asset, boolean updated, SecurityUser user) {
130 169 try {
131   - logEntityAction(asset.getId(), asset,
  170 + logEntityAction(user, asset.getId(), asset,
132 171 asset.getCustomerId(),
133 172 updated ? ActionType.UPDATED : ActionType.ADDED, null);
134 173 } catch (ThingsboardException e) {
... ... @@ -140,10 +179,12 @@ public class AssetController extends BaseController {
140 179 }
141 180 }
142 181
  182 + @ApiOperation(value = "Delete asset (deleteAsset)",
  183 + 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)
143 184 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
144 185 @RequestMapping(value = "/asset/{assetId}", method = RequestMethod.DELETE)
145 186 @ResponseStatus(value = HttpStatus.OK)
146   - public void deleteAsset(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  187 + public void deleteAsset(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
147 188 checkParameter(ASSET_ID, strAssetId);
148 189 try {
149 190 AssetId assetId = new AssetId(toUUID(strAssetId));
... ... @@ -167,11 +208,13 @@ public class AssetController extends BaseController {
167 208 }
168 209 }
169 210
  211 + @ApiOperation(value = "Assign asset to customer (assignAssetToCustomer)",
  212 + notes = "Creates assignment of the asset to customer. Customer will be able to query asset afterwards." + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
170 213 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
171 214 @RequestMapping(value = "/customer/{customerId}/asset/{assetId}", method = RequestMethod.POST)
172 215 @ResponseBody
173   - public Asset assignAssetToCustomer(@PathVariable("customerId") String strCustomerId,
174   - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  216 + public Asset assignAssetToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION) @PathVariable("customerId") String strCustomerId,
  217 + @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
175 218 checkParameter("customerId", strCustomerId);
176 219 checkParameter(ASSET_ID, strAssetId);
177 220 try {
... ... @@ -201,10 +244,12 @@ public class AssetController extends BaseController {
201 244 }
202 245 }
203 246
  247 + @ApiOperation(value = "Unassign asset from customer (unassignAssetFromCustomer)",
  248 + 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)
204 249 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
205 250 @RequestMapping(value = "/customer/asset/{assetId}", method = RequestMethod.DELETE)
206 251 @ResponseBody
207   - public Asset unassignAssetFromCustomer(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  252 + public Asset unassignAssetFromCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
208 253 checkParameter(ASSET_ID, strAssetId);
209 254 try {
210 255 AssetId assetId = new AssetId(toUUID(strAssetId));
... ... @@ -235,10 +280,14 @@ public class AssetController extends BaseController {
235 280 }
236 281 }
237 282
  283 + @ApiOperation(value = "Make asset publicly available (assignAssetToPublicCustomer)",
  284 + notes = "Asset will be available for non-authorized (not logged-in) users. " +
  285 + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
  286 + "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)
238 287 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
239 288 @RequestMapping(value = "/customer/public/asset/{assetId}", method = RequestMethod.POST)
240 289 @ResponseBody
241   - public Asset assignAssetToPublicCustomer(@PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  290 + public Asset assignAssetToPublicCustomer(@ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
242 291 checkParameter(ASSET_ID, strAssetId);
243 292 try {
244 293 AssetId assetId = new AssetId(toUUID(strAssetId));
... ... @@ -261,15 +310,24 @@ public class AssetController extends BaseController {
261 310 }
262 311 }
263 312
  313 + @ApiOperation(value = "Get Tenant Assets (getTenantAssets)",
  314 + notes = "Returns a page of assets owned by tenant. " +
  315 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
264 316 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
265 317 @RequestMapping(value = "/tenant/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
266 318 @ResponseBody
267 319 public PageData<Asset> getTenantAssets(
  320 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
268 321 @RequestParam int pageSize,
  322 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
269 323 @RequestParam int page,
  324 + @ApiParam(value = ASSET_TYPE_DESCRIPTION)
270 325 @RequestParam(required = false) String type,
  326 + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
271 327 @RequestParam(required = false) String textSearch,
  328 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
272 329 @RequestParam(required = false) String sortProperty,
  330 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
273 331 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
274 332 try {
275 333 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -284,15 +342,24 @@ public class AssetController extends BaseController {
284 342 }
285 343 }
286 344
  345 + @ApiOperation(value = "Get Tenant Asset Infos (getTenantAssetInfos)",
  346 + notes = "Returns a page of assets info objects owned by tenant. " +
  347 + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
287 348 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
288 349 @RequestMapping(value = "/tenant/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
289 350 @ResponseBody
290 351 public PageData<AssetInfo> getTenantAssetInfos(
  352 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
291 353 @RequestParam int pageSize,
  354 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
292 355 @RequestParam int page,
  356 + @ApiParam(value = ASSET_TYPE_DESCRIPTION)
293 357 @RequestParam(required = false) String type,
  358 + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
294 359 @RequestParam(required = false) String textSearch,
  360 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
295 361 @RequestParam(required = false) String sortProperty,
  362 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
296 363 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
297 364 try {
298 365 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -307,10 +374,14 @@ public class AssetController extends BaseController {
307 374 }
308 375 }
309 376
  377 + @ApiOperation(value = "Get Tenant Asset (getTenantAsset)",
  378 + notes = "Requested asset must be owned by tenant that the user belongs to. " +
  379 + "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)
310 380 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
311 381 @RequestMapping(value = "/tenant/assets", params = {"assetName"}, method = RequestMethod.GET)
312 382 @ResponseBody
313 383 public Asset getTenantAsset(
  384 + @ApiParam(value = ASSET_NAME_DESCRIPTION)
314 385 @RequestParam String assetName) throws ThingsboardException {
315 386 try {
316 387 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -320,16 +391,26 @@ public class AssetController extends BaseController {
320 391 }
321 392 }
322 393
  394 + @ApiOperation(value = "Get Customer Assets (getCustomerAssets)",
  395 + notes = "Returns a page of assets objects assigned to customer. " +
  396 + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE)
323 397 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
324 398 @RequestMapping(value = "/customer/{customerId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
325 399 @ResponseBody
326 400 public PageData<Asset> getCustomerAssets(
  401 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
327 402 @PathVariable("customerId") String strCustomerId,
  403 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
328 404 @RequestParam int pageSize,
  405 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
329 406 @RequestParam int page,
  407 + @ApiParam(value = ASSET_TYPE_DESCRIPTION)
330 408 @RequestParam(required = false) String type,
  409 + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
331 410 @RequestParam(required = false) String textSearch,
  411 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
332 412 @RequestParam(required = false) String sortProperty,
  413 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
333 414 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
334 415 checkParameter("customerId", strCustomerId);
335 416 try {
... ... @@ -347,16 +428,26 @@ public class AssetController extends BaseController {
347 428 }
348 429 }
349 430
  431 + @ApiOperation(value = "Get Customer Asset Infos (getCustomerAssetInfos)",
  432 + notes = "Returns a page of assets info objects assigned to customer. " +
  433 + PAGE_DATA_PARAMETERS + ASSET_INFO_DESCRIPTION, produces = MediaType.APPLICATION_JSON_VALUE)
350 434 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
351 435 @RequestMapping(value = "/customer/{customerId}/assetInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
352 436 @ResponseBody
353 437 public PageData<AssetInfo> getCustomerAssetInfos(
  438 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
354 439 @PathVariable("customerId") String strCustomerId,
  440 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
355 441 @RequestParam int pageSize,
  442 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
356 443 @RequestParam int page,
  444 + @ApiParam(value = ASSET_TYPE_DESCRIPTION)
357 445 @RequestParam(required = false) String type,
  446 + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
358 447 @RequestParam(required = false) String textSearch,
  448 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
359 449 @RequestParam(required = false) String sortProperty,
  450 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
360 451 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
361 452 checkParameter("customerId", strCustomerId);
362 453 try {
... ... @@ -374,10 +465,13 @@ public class AssetController extends BaseController {
374 465 }
375 466 }
376 467
  468 + @ApiOperation(value = "Get Assets By Ids (getAssetsByIds)",
  469 + notes = "Requested assets must be owned by tenant or assigned to customer which user is performing the request. ", produces = MediaType.APPLICATION_JSON_VALUE)
377 470 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
378 471 @RequestMapping(value = "/assets", params = {"assetIds"}, method = RequestMethod.GET)
379 472 @ResponseBody
380 473 public List<Asset> getAssetsByIds(
  474 + @ApiParam(value = "A list of assets ids, separated by comma ','")
381 475 @RequestParam("assetIds") String[] strAssetIds) throws ThingsboardException {
382 476 checkArrayParameter("assetIds", strAssetIds);
383 477 try {
... ... @@ -400,6 +494,10 @@ public class AssetController extends BaseController {
400 494 }
401 495 }
402 496
  497 + @ApiOperation(value = "Find related assets (findByQuery)",
  498 + notes = "Returns all assets that are related to the specific entity. " +
  499 + "The entity id, relation type, asset types, depth of the search, and other query parameters defined using complex 'AssetSearchQuery' object. " +
  500 + "See 'Model' tab of the Parameters for more info.", produces = MediaType.APPLICATION_JSON_VALUE)
403 501 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
404 502 @RequestMapping(value = "/assets", method = RequestMethod.POST)
405 503 @ResponseBody
... ... @@ -424,6 +522,8 @@ public class AssetController extends BaseController {
424 522 }
425 523 }
426 524
  525 + @ApiOperation(value = "Get Asset Types (getAssetTypes)",
  526 + notes = "Returns a set of unique asset types based on assets that are either owned by the tenant or assigned to the customer which user is performing the request.", produces = MediaType.APPLICATION_JSON_VALUE)
427 527 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
428 528 @RequestMapping(value = "/asset/types", method = RequestMethod.GET)
429 529 @ResponseBody
... ... @@ -438,11 +538,18 @@ public class AssetController extends BaseController {
438 538 }
439 539 }
440 540
  541 + @ApiOperation(value = "Assign asset to edge (assignAssetToEdge)",
  542 + notes = "Creates assignment of an existing asset to an instance of The Edge. " +
  543 + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
  544 + "Second, remote edge service will receive a copy of assignment asset " +
  545 + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " +
  546 + "Third, once asset will be delivered to edge service, it's going to be available for usage on remote edge instance.",
  547 + produces = MediaType.APPLICATION_JSON_VALUE)
441 548 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
442 549 @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.POST)
443 550 @ResponseBody
444   - public Asset assignAssetToEdge(@PathVariable(EDGE_ID) String strEdgeId,
445   - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  551 + public Asset assignAssetToEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId,
  552 + @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
446 553 checkParameter(EDGE_ID, strEdgeId);
447 554 checkParameter(ASSET_ID, strAssetId);
448 555 try {
... ... @@ -471,11 +578,18 @@ public class AssetController extends BaseController {
471 578 }
472 579 }
473 580
  581 + @ApiOperation(value = "Unassign asset from edge (unassignAssetFromEdge)",
  582 + notes = "Clears assignment of the asset to the edge. " +
  583 + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
  584 + "Second, remote edge service will receive an 'unassign' command to remove asset " +
  585 + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " +
  586 + "Third, once 'unassign' command will be delivered to edge service, it's going to remove asset locally.",
  587 + produces = MediaType.APPLICATION_JSON_VALUE)
474 588 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
475 589 @RequestMapping(value = "/edge/{edgeId}/asset/{assetId}", method = RequestMethod.DELETE)
476 590 @ResponseBody
477   - public Asset unassignAssetFromEdge(@PathVariable(EDGE_ID) String strEdgeId,
478   - @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
  591 + public Asset unassignAssetFromEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION) @PathVariable(EDGE_ID) String strEdgeId,
  592 + @ApiParam(value = ASSET_ID_PARAM_DESCRIPTION) @PathVariable(ASSET_ID) String strAssetId) throws ThingsboardException {
479 593 checkParameter(EDGE_ID, strEdgeId);
480 594 checkParameter(ASSET_ID, strAssetId);
481 595 try {
... ... @@ -504,18 +618,30 @@ public class AssetController extends BaseController {
504 618 }
505 619 }
506 620
  621 + @ApiOperation(value = "Get assets assigned to edge (getEdgeAssets)",
  622 + notes = "Returns a page of assets assigned to edge. " +
  623 + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE)
507 624 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
508 625 @RequestMapping(value = "/edge/{edgeId}/assets", params = {"pageSize", "page"}, method = RequestMethod.GET)
509 626 @ResponseBody
510 627 public PageData<Asset> getEdgeAssets(
  628 + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION)
511 629 @PathVariable(EDGE_ID) String strEdgeId,
  630 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
512 631 @RequestParam int pageSize,
  632 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
513 633 @RequestParam int page,
  634 + @ApiParam(value = ASSET_TYPE_DESCRIPTION)
514 635 @RequestParam(required = false) String type,
  636 + @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
515 637 @RequestParam(required = false) String textSearch,
  638 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
516 639 @RequestParam(required = false) String sortProperty,
  640 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
517 641 @RequestParam(required = false) String sortOrder,
  642 + @ApiParam(value = "Timestamp. Assets with creation time before it won't be queried")
518 643 @RequestParam(required = false) Long startTime,
  644 + @ApiParam(value = "Timestamp. Assets with creation time after it won't be queried")
519 645 @RequestParam(required = false) Long endTime) throws ThingsboardException {
520 646 checkParameter(EDGE_ID, strEdgeId);
521 647 try {
... ... @@ -547,11 +673,14 @@ public class AssetController extends BaseController {
547 673 }
548 674 }
549 675
  676 + @ApiOperation(value = "Import the bulk of assets (processAssetsBulkImport)",
  677 + notes = "There's an ability to import the bulk of assets using the only .csv file.", produces = MediaType.APPLICATION_JSON_VALUE)
550 678 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
551 679 @PostMapping("/asset/bulk_import")
552 680 public BulkImportResult<Asset> processAssetsBulkImport(@RequestBody BulkImportRequest request) throws Exception {
553   - return assetBulkImportService.processBulkImport(request, getCurrentUser(), importedAssetInfo -> {
554   - onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated());
  681 + SecurityUser user = getCurrentUser();
  682 + return assetBulkImportService.processBulkImport(request, user, importedAssetInfo -> {
  683 + onAssetCreatedOrUpdated(importedAssetInfo.getEntity(), importedAssetInfo.isUpdated(), user);
555 684 });
556 685 }
557 686
... ...
... ... @@ -15,7 +15,10 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import io.swagger.annotations.ApiOperation;
  19 +import io.swagger.annotations.ApiParam;
18 20 import org.apache.commons.lang3.StringUtils;
  21 +import org.springframework.http.MediaType;
19 22 import org.springframework.security.access.prepost.PreAuthorize;
20 23 import org.springframework.web.bind.annotation.PathVariable;
21 24 import org.springframework.web.bind.annotation.RequestMapping;
... ... @@ -39,23 +42,60 @@ import java.util.List;
39 42 import java.util.UUID;
40 43 import java.util.stream.Collectors;
41 44
  45 +import static org.thingsboard.server.controller.ControllerConstants.AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES;
  46 +import static org.thingsboard.server.controller.ControllerConstants.AUDIT_LOG_TEXT_SEARCH_DESCRIPTION;
  47 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
  48 +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
  49 +import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
  50 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  51 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  52 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  53 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  54 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  55 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  56 +import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION;
  57 +
42 58 @RestController
43 59 @TbCoreComponent
44 60 @RequestMapping("/api")
45 61 public class AuditLogController extends BaseController {
46 62
  63 + 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'.";
  64 + 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'.";
  65 + private static final String AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION = "A String value representing comma-separated list of action types. " +
  66 + "This parameter is optional, but it can be used to filter results to fetch only audit logs of specific action types. " +
  67 + "For example, 'LOGIN', 'LOGOUT'. See the 'Model' tab of the Response Class for more details.";
  68 + private static final String AUDIT_LOG_SORT_PROPERTY_DESCRIPTION = "Property of audit log to sort by. " +
  69 + "See the 'Model' tab of the Response Class for more details. " +
  70 + "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.";
  71 +
  72 +
  73 + @ApiOperation(value = "Get audit logs by customer id (getAuditLogsByCustomerId)",
  74 + notes = "Returns a page of audit logs related to the targeted customer entities (devices, assets, etc.), " +
  75 + "and users actions (login, logout, etc.) that belong to this customer. " +
  76 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
  77 + produces = MediaType.APPLICATION_JSON_VALUE)
47 78 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
48 79 @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
49 80 @ResponseBody
50 81 public PageData<AuditLog> getAuditLogsByCustomerId(
  82 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
51 83 @PathVariable("customerId") String strCustomerId,
  84 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
52 85 @RequestParam int pageSize,
  86 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
53 87 @RequestParam int page,
  88 + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
54 89 @RequestParam(required = false) String textSearch,
  90 + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
55 91 @RequestParam(required = false) String sortProperty,
  92 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
56 93 @RequestParam(required = false) String sortOrder,
  94 + @ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
57 95 @RequestParam(required = false) Long startTime,
  96 + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
58 97 @RequestParam(required = false) Long endTime,
  98 + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
59 99 @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
60 100 try {
61 101 checkParameter("CustomerId", strCustomerId);
... ... @@ -68,18 +108,32 @@ public class AuditLogController extends BaseController {
68 108 }
69 109 }
70 110
  111 + @ApiOperation(value = "Get audit logs by user id (getAuditLogsByUserId)",
  112 + notes = "Returns a page of audit logs related to the actions of targeted user. " +
  113 + "For example, RPC call to a particular device, or alarm acknowledgment for a specific device, etc. " +
  114 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
  115 + produces = MediaType.APPLICATION_JSON_VALUE)
71 116 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
72 117 @RequestMapping(value = "/audit/logs/user/{userId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
73 118 @ResponseBody
74 119 public PageData<AuditLog> getAuditLogsByUserId(
  120 + @ApiParam(value = USER_ID_PARAM_DESCRIPTION)
75 121 @PathVariable("userId") String strUserId,
  122 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
76 123 @RequestParam int pageSize,
  124 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
77 125 @RequestParam int page,
  126 + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
78 127 @RequestParam(required = false) String textSearch,
  128 + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
79 129 @RequestParam(required = false) String sortProperty,
  130 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
80 131 @RequestParam(required = false) String sortOrder,
  132 + @ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
81 133 @RequestParam(required = false) Long startTime,
  134 + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
82 135 @RequestParam(required = false) Long endTime,
  136 + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
83 137 @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
84 138 try {
85 139 checkParameter("UserId", strUserId);
... ... @@ -92,19 +146,35 @@ public class AuditLogController extends BaseController {
92 146 }
93 147 }
94 148
  149 + @ApiOperation(value = "Get audit logs by entity id (getAuditLogsByEntityId)",
  150 + notes = "Returns a page of audit logs related to the actions on the targeted entity. " +
  151 + "Basically, this API call is used to get the full lifecycle of some specific entity. " +
  152 + "For example to see when a device was created, updated, assigned to some customer, or even deleted from the system. " +
  153 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
  154 + produces = MediaType.APPLICATION_JSON_VALUE)
95 155 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
96 156 @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
97 157 @ResponseBody
98 158 public PageData<AuditLog> getAuditLogsByEntityId(
  159 + @ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION)
99 160 @PathVariable("entityType") String strEntityType,
  161 + @ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION)
100 162 @PathVariable("entityId") String strEntityId,
  163 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
101 164 @RequestParam int pageSize,
  165 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
102 166 @RequestParam int page,
  167 + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
103 168 @RequestParam(required = false) String textSearch,
  169 + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
104 170 @RequestParam(required = false) String sortProperty,
  171 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
105 172 @RequestParam(required = false) String sortOrder,
  173 + @ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
106 174 @RequestParam(required = false) Long startTime,
  175 + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
107 176 @RequestParam(required = false) Long endTime,
  177 + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
108 178 @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
109 179 try {
110 180 checkParameter("EntityId", strEntityId);
... ... @@ -118,17 +188,29 @@ public class AuditLogController extends BaseController {
118 188 }
119 189 }
120 190
  191 + @ApiOperation(value = "Get all audit logs (getAuditLogs)",
  192 + notes = "Returns a page of audit logs related to all entities in the scope of the current user's Tenant. " +
  193 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
  194 + produces = MediaType.APPLICATION_JSON_VALUE)
121 195 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
122 196 @RequestMapping(value = "/audit/logs", params = {"pageSize", "page"}, method = RequestMethod.GET)
123 197 @ResponseBody
124 198 public PageData<AuditLog> getAuditLogs(
  199 + @ApiParam(value = PAGE_SIZE_DESCRIPTION)
125 200 @RequestParam int pageSize,
  201 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION)
126 202 @RequestParam int page,
  203 + @ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
127 204 @RequestParam(required = false) String textSearch,
  205 + @ApiParam(value = AUDIT_LOG_SORT_PROPERTY_DESCRIPTION, allowableValues = AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES)
128 206 @RequestParam(required = false) String sortProperty,
  207 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
129 208 @RequestParam(required = false) String sortOrder,
  209 + @ApiParam(value = AUDIT_LOG_QUERY_START_TIME_DESCRIPTION)
130 210 @RequestParam(required = false) Long startTime,
  211 + @ApiParam(value = AUDIT_LOG_QUERY_END_TIME_DESCRIPTION)
131 212 @RequestParam(required = false) Long endTime,
  213 + @ApiParam(value = AUDIT_LOG_QUERY_ACTION_TYPES_DESCRIPTION)
132 214 @RequestParam(name = "actionTypes", required = false) String actionTypesStr) throws ThingsboardException {
133 215 try {
134 216 TenantId tenantId = getCurrentUser().getTenantId();
... ...
... ... @@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
18 18 import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 20 import com.fasterxml.jackson.databind.node.ObjectNode;
  21 +import io.swagger.annotations.ApiOperation;
  22 +import io.swagger.annotations.ApiParam;
21 23 import lombok.RequiredArgsConstructor;
22 24 import lombok.extern.slf4j.Slf4j;
23 25 import org.springframework.context.ApplicationEventPublisher;
... ... @@ -48,8 +50,13 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
48 50 import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
49 51 import org.thingsboard.server.dao.audit.AuditLogService;
50 52 import org.thingsboard.server.queue.util.TbCoreComponent;
  53 +import org.thingsboard.server.service.security.model.ActivateUserRequest;
  54 +import org.thingsboard.server.service.security.model.ChangePasswordRequest;
  55 +import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
  56 +import org.thingsboard.server.service.security.model.ResetPasswordRequest;
51 57 import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
52 58 import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
  59 +import org.thingsboard.server.service.security.model.JwtTokenPair;
53 60 import org.thingsboard.server.service.security.model.SecurityUser;
54 61 import org.thingsboard.server.service.security.model.UserPrincipal;
55 62 import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
... ... @@ -74,9 +81,13 @@ public class AuthController extends BaseController {
74 81 private final AuditLogService auditLogService;
75 82 private final ApplicationEventPublisher eventPublisher;
76 83
  84 +
  85 + @ApiOperation(value = "Get current User (getUser)",
  86 + notes = "Get the information about the User which credentials are used to perform this REST API call.")
77 87 @PreAuthorize("isAuthenticated()")
78 88 @RequestMapping(value = "/auth/user", method = RequestMethod.GET)
79   - public @ResponseBody User getUser() throws ThingsboardException {
  89 + public @ResponseBody
  90 + User getUser() throws ThingsboardException {
80 91 try {
81 92 SecurityUser securityUser = getCurrentUser();
82 93 return userService.findUserById(securityUser.getTenantId(), securityUser.getId());
... ... @@ -85,6 +96,8 @@ public class AuthController extends BaseController {
85 96 }
86 97 }
87 98
  99 + @ApiOperation(value = "Logout (logout)",
  100 + notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ")
88 101 @PreAuthorize("isAuthenticated()")
89 102 @RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
90 103 @ResponseStatus(value = HttpStatus.OK)
... ... @@ -92,13 +105,17 @@ public class AuthController extends BaseController {
92 105 logLogoutAction(request);
93 106 }
94 107
  108 + @ApiOperation(value = "Change password for current User (changePassword)",
  109 + notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.")
95 110 @PreAuthorize("isAuthenticated()")
96 111 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
97 112 @ResponseStatus(value = HttpStatus.OK)
98   - public ObjectNode changePassword(@RequestBody JsonNode changePasswordRequest) throws ThingsboardException {
  113 + public ObjectNode changePassword(
  114 + @ApiParam(value = "Change Password Request")
  115 + @RequestBody ChangePasswordRequest changePasswordRequest) throws ThingsboardException {
99 116 try {
100   - String currentPassword = changePasswordRequest.get("currentPassword").asText();
101   - String newPassword = changePasswordRequest.get("newPassword").asText();
  117 + String currentPassword = changePasswordRequest.getCurrentPassword();
  118 + String newPassword = changePasswordRequest.getNewPassword();
102 119 SecurityUser securityUser = getCurrentUser();
103 120 UserCredentials userCredentials = userService.findUserCredentialsByUserId(TenantId.SYS_TENANT_ID, securityUser.getId());
104 121 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
... ... @@ -123,6 +140,8 @@ public class AuthController extends BaseController {
123 140 }
124 141 }
125 142
  143 + @ApiOperation(value = "Get the current User password policy (getUserPasswordPolicy)",
  144 + notes = "API call to get the password policy for the password validation form(s).")
126 145 @RequestMapping(value = "/noauth/userPasswordPolicy", method = RequestMethod.GET)
127 146 @ResponseBody
128 147 public UserPasswordPolicy getUserPasswordPolicy() throws ThingsboardException {
... ... @@ -135,8 +154,13 @@ public class AuthController extends BaseController {
135 154 }
136 155 }
137 156
  157 + @ApiOperation(value = "Check Activate User Token (checkActivateToken)",
  158 + notes = "Checks the activation token and forwards user to 'Create Password' page. " +
  159 + "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Create Password' page and same 'activateToken' specified in the URL parameters. " +
  160 + "If token is not valid, returns '409 Conflict'.")
138 161 @RequestMapping(value = "/noauth/activate", params = {"activateToken"}, method = RequestMethod.GET)
139 162 public ResponseEntity<String> checkActivateToken(
  163 + @ApiParam(value = "The activate token string.")
140 164 @RequestParam(value = "activateToken") String activateToken) {
141 165 HttpHeaders headers = new HttpHeaders();
142 166 HttpStatus responseStatus;
... ... @@ -157,13 +181,17 @@ public class AuthController extends BaseController {
157 181 return new ResponseEntity<>(headers, responseStatus);
158 182 }
159 183
  184 + @ApiOperation(value = "Request reset password email (requestResetPasswordByEmail)",
  185 + notes = "Request to send the reset password email if the user with specified email address is present in the database. " +
  186 + "Always return '200 OK' status for security purposes.")
160 187 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
161 188 @ResponseStatus(value = HttpStatus.OK)
162 189 public void requestResetPasswordByEmail(
163   - @RequestBody JsonNode resetPasswordByEmailRequest,
  190 + @ApiParam(value = "The JSON object representing the reset password email request.")
  191 + @RequestBody ResetPasswordEmailRequest resetPasswordByEmailRequest,
164 192 HttpServletRequest request) throws ThingsboardException {
165 193 try {
166   - String email = resetPasswordByEmailRequest.get("email").asText();
  194 + String email = resetPasswordByEmailRequest.getEmail();
167 195 UserCredentials userCredentials = userService.requestPasswordReset(TenantId.SYS_TENANT_ID, email);
168 196 User user = userService.findUserById(TenantId.SYS_TENANT_ID, userCredentials.getUserId());
169 197 String baseUrl = systemSecurityService.getBaseUrl(user.getTenantId(), user.getCustomerId(), request);
... ... @@ -176,8 +204,13 @@ public class AuthController extends BaseController {
176 204 }
177 205 }
178 206
  207 + @ApiOperation(value = "Check password reset token (checkResetToken)",
  208 + notes = "Checks the password reset token and forwards user to 'Reset Password' page. " +
  209 + "If token is valid, returns '303 See Other' (redirect) response code with the correct address of 'Reset Password' page and same 'resetToken' specified in the URL parameters. " +
  210 + "If token is not valid, returns '409 Conflict'.")
179 211 @RequestMapping(value = "/noauth/resetPassword", params = {"resetToken"}, method = RequestMethod.GET)
180 212 public ResponseEntity<String> checkResetToken(
  213 + @ApiParam(value = "The reset token string.")
181 214 @RequestParam(value = "resetToken") String resetToken) {
182 215 HttpHeaders headers = new HttpHeaders();
183 216 HttpStatus responseStatus;
... ... @@ -198,16 +231,24 @@ public class AuthController extends BaseController {
198 231 return new ResponseEntity<>(headers, responseStatus);
199 232 }
200 233
  234 + @ApiOperation(value = "Activate User",
  235 + notes = "Checks the activation token and updates corresponding user password in the database. " +
  236 + "Now the user may start using his password to login. " +
  237 + "The response already contains the [JWT](https://jwt.io) activation and refresh tokens, " +
  238 + "to simplify the user activation flow and avoid asking user to input password again after activation. " +
  239 + "If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
  240 + "If token is not valid, returns '404 Bad Request'.")
201 241 @RequestMapping(value = "/noauth/activate", method = RequestMethod.POST)
202 242 @ResponseStatus(value = HttpStatus.OK)
203 243 @ResponseBody
204   - public JsonNode activateUser(
205   - @RequestBody JsonNode activateRequest,
  244 + public JwtTokenPair activateUser(
  245 + @ApiParam(value = "Activate user request.")
  246 + @RequestBody ActivateUserRequest activateRequest,
206 247 @RequestParam(required = false, defaultValue = "true") boolean sendActivationMail,
207 248 HttpServletRequest request) throws ThingsboardException {
208 249 try {
209   - String activateToken = activateRequest.get("activateToken").asText();
210   - String password = activateRequest.get("password").asText();
  250 + String activateToken = activateRequest.getActivateToken();
  251 + String password = activateRequest.getPassword();
211 252 systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, null);
212 253 String encodedPassword = passwordEncoder.encode(password);
213 254 UserCredentials credentials = userService.activateUserCredentials(TenantId.SYS_TENANT_ID, activateToken, encodedPassword);
... ... @@ -232,25 +273,26 @@ public class AuthController extends BaseController {
232 273 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
233 274 JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
234 275
235   - ObjectMapper objectMapper = new ObjectMapper();
236   - ObjectNode tokenObject = objectMapper.createObjectNode();
237   - tokenObject.put("token", accessToken.getToken());
238   - tokenObject.put("refreshToken", refreshToken.getToken());
239   - return tokenObject;
  276 + return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
240 277 } catch (Exception e) {
241 278 throw handleException(e);
242 279 }
243 280 }
244 281
  282 + @ApiOperation(value = "Reset password (resetPassword)",
  283 + notes = "Checks the password reset token and updates the password. " +
  284 + "If token is valid, returns the object that contains [JWT](https://jwt.io/) access and refresh tokens. " +
  285 + "If token is not valid, returns '404 Bad Request'.")
245 286 @RequestMapping(value = "/noauth/resetPassword", method = RequestMethod.POST)
246 287 @ResponseStatus(value = HttpStatus.OK)
247 288 @ResponseBody
248   - public JsonNode resetPassword(
249   - @RequestBody JsonNode resetPasswordRequest,
  289 + public JwtTokenPair resetPassword(
  290 + @ApiParam(value = "Reset password request.")
  291 + @RequestBody ResetPasswordRequest resetPasswordRequest,
250 292 HttpServletRequest request) throws ThingsboardException {
251 293 try {
252   - String resetToken = resetPasswordRequest.get("resetToken").asText();
253   - String password = resetPasswordRequest.get("password").asText();
  294 + String resetToken = resetPasswordRequest.getResetToken();
  295 + String password = resetPasswordRequest.getPassword();
254 296 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken);
255 297 if (userCredentials != null) {
256 298 systemSecurityService.validatePassword(TenantId.SYS_TENANT_ID, password, userCredentials);
... ... @@ -273,11 +315,7 @@ public class AuthController extends BaseController {
273 315 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
274 316 JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
275 317
276   - ObjectMapper objectMapper = new ObjectMapper();
277   - ObjectNode tokenObject = objectMapper.createObjectNode();
278   - tokenObject.put("token", accessToken.getToken());
279   - tokenObject.put("refreshToken", refreshToken.getToken());
280   - return tokenObject;
  318 + return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
281 319 } else {
282 320 throw new ThingsboardException("Invalid reset token!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
283 321 }
... ...
... ... @@ -147,17 +147,15 @@ import java.util.Optional;
147 147 import java.util.Set;
148 148 import java.util.UUID;
149 149
  150 +import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_PAGE_SIZE;
  151 +import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID;
150 152 import static org.thingsboard.server.dao.service.Validator.validateId;
151 153
152 154 @Slf4j
153 155 @TbCoreComponent
154 156 public abstract class BaseController {
155 157
156   - public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
157   - protected static final String DEFAULT_DASHBOARD = "defaultDashboardId";
158   - protected static final String HOME_DASHBOARD = "homeDashboardId";
159   -
160   - private static final int DEFAULT_PAGE_SIZE = 1000;
  158 + /*Swagger UI description*/
161 159
162 160 private static final ObjectMapper json = new ObjectMapper();
163 161
... ... @@ -897,7 +895,7 @@ public abstract class BaseController {
897 895 PageDataIterableByTenantIdEntityId<EdgeId> relatedEdgeIdsIterator =
898 896 new PageDataIterableByTenantIdEntityId<>(edgeService::findRelatedEdgeIdsByEntityId, tenantId, entityId, DEFAULT_PAGE_SIZE);
899 897 List<EdgeId> result = new ArrayList<>();
900   - for(EdgeId edgeId : relatedEdgeIdsIterator) {
  898 + for (EdgeId edgeId : relatedEdgeIdsIterator) {
901 899 result.add(edgeId);
902 900 }
903 901 return result;
... ...
... ... @@ -15,6 +15,8 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import io.swagger.annotations.ApiOperation;
  19 +import io.swagger.annotations.ApiParam;
18 20 import org.apache.commons.lang3.StringUtils;
19 21 import org.springframework.security.access.prepost.PreAuthorize;
20 22 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -33,15 +35,27 @@ import java.util.HashSet;
33 35 import java.util.List;
34 36 import java.util.Set;
35 37
  38 +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
  39 +
36 40 @RestController
37 41 @TbCoreComponent
38 42 @RequestMapping("/api")
39 43 public class ComponentDescriptorController extends BaseController {
40 44
  45 + private static final String COMPONENT_DESCRIPTOR_DEFINITION = "Each Component Descriptor represents configuration of specific rule node (e.g. 'Save Timeseries' or 'Send Email'.). " +
  46 + "The Component Descriptors are used by the rule chain Web UI to build the configuration forms for the rule nodes. " +
  47 + "The Component Descriptors are discovered at runtime by scanning the class path and searching for @RuleNode annotation. " +
  48 + "Once discovered, the up to date list of descriptors is persisted to the database.";
  49 +
  50 + @ApiOperation(value = "Get Component Descriptor (getComponentDescriptorByClazz)",
  51 + notes = "Gets the Component Descriptor object using class name from the path parameters. " +
  52 + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
41 53 @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
42 54 @RequestMapping(value = "/component/{componentDescriptorClazz:.+}", method = RequestMethod.GET)
43 55 @ResponseBody
44   - public ComponentDescriptor getComponentDescriptorByClazz(@PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException {
  56 + public ComponentDescriptor getComponentDescriptorByClazz(
  57 + @ApiParam(value = "Component Descriptor class name", required = true)
  58 + @PathVariable("componentDescriptorClazz") String strComponentDescriptorClazz) throws ThingsboardException {
45 59 checkParameter("strComponentDescriptorClazz", strComponentDescriptorClazz);
46 60 try {
47 61 return checkComponentDescriptorByClazz(strComponentDescriptorClazz);
... ... @@ -50,11 +64,17 @@ public class ComponentDescriptorController extends BaseController {
50 64 }
51 65 }
52 66
  67 + @ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByType)",
  68 + notes = "Gets the Component Descriptors using rule node type and optional rule chain type request parameters. " +
  69 + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
53 70 @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
54 71 @RequestMapping(value = "/components/{componentType}", method = RequestMethod.GET)
55 72 @ResponseBody
56   - public List<ComponentDescriptor> getComponentDescriptorsByType(@PathVariable("componentType") String strComponentType,
57   - @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
  73 + public List<ComponentDescriptor> getComponentDescriptorsByType(
  74 + @ApiParam(value = "Type of the Rule Node", allowableValues = "ENRICHMENT,FILTER,TRANSFORMATION,ACTION,EXTERNAL", required = true)
  75 + @PathVariable("componentType") String strComponentType,
  76 + @ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE")
  77 + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
58 78 checkParameter("componentType", strComponentType);
59 79 try {
60 80 return checkComponentDescriptorsByType(ComponentType.valueOf(strComponentType), getRuleChainType(strRuleChainType));
... ... @@ -63,11 +83,17 @@ public class ComponentDescriptorController extends BaseController {
63 83 }
64 84 }
65 85
  86 + @ApiOperation(value = "Get Component Descriptors (getComponentDescriptorsByTypes)",
  87 + notes = "Gets the Component Descriptors using coma separated list of rule node types and optional rule chain type request parameters. " +
  88 + COMPONENT_DESCRIPTOR_DEFINITION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
66 89 @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')")
67 90 @RequestMapping(value = "/components", params = {"componentTypes"}, method = RequestMethod.GET)
68 91 @ResponseBody
69   - public List<ComponentDescriptor> getComponentDescriptorsByTypes(@RequestParam("componentTypes") String[] strComponentTypes,
70   - @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
  92 + public List<ComponentDescriptor> getComponentDescriptorsByTypes(
  93 + @ApiParam(value = "List of types of the Rule Nodes, (ENRICHMENT, FILTER, TRANSFORMATION, ACTION or EXTERNAL)", required = true)
  94 + @RequestParam("componentTypes") String[] strComponentTypes,
  95 + @ApiParam(value = "Type of the Rule Chain", allowableValues = "CORE,EDGE")
  96 + @RequestParam(value = "ruleChainType", required = false) String strRuleChainType) throws ThingsboardException {
71 97 checkArrayParameter("componentTypes", strComponentTypes);
72 98 try {
73 99 Set<ComponentType> componentTypes = new HashSet<>();
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.controller;
  17 +
  18 +public class ControllerConstants {
  19 +
  20 +
  21 + protected static final String NEW_LINE = "\n\n";
  22 + protected static final int DEFAULT_PAGE_SIZE = 1000;
  23 + protected static final String ENTITY_TYPE = "entityType";
  24 + protected static final String CUSTOMER_ID = "customerId";
  25 + protected static final String TENANT_ID = "tenantId";
  26 + protected static final String DEVICE_ID = "deviceId";
  27 + protected static final String RPC_ID = "rpcId";
  28 + protected static final String ENTITY_ID = "entityId";
  29 + protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " +
  30 + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " +
  31 + "See the 'Model' tab of the Response Class for more details. ";
  32 + protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  33 + protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  34 + protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  35 + protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  36 + protected static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  37 + protected static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  38 + protected static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  39 + protected static final String CUSTOMER_ID_PARAM_DESCRIPTION = "A string value representing the customer id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  40 + protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  41 + protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  42 + protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  43 + protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  44 + protected static final String OTA_PACKAGE_ID_PARAM_DESCRIPTION = "A string value representing the ota package id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  45 + protected static final String ENTITY_TYPE_PARAM_DESCRIPTION = "A string value representing the entity type. For example, 'DEVICE'";
  46 + protected static final String RULE_CHAIN_ID_PARAM_DESCRIPTION = "A string value representing the rule chain id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  47 + protected static final String WIDGET_BUNDLE_ID_PARAM_DESCRIPTION = "A string value representing the widget bundle id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  48 + protected static final String WIDGET_TYPE_ID_PARAM_DESCRIPTION = "A string value representing the widget type id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  49 + protected static final String RESOURCE_ID_PARAM_DESCRIPTION = "A string value representing the resource id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
  50 + protected static final String SYSTEM_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' authority.";
  51 + protected static final String SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'SYS_ADMIN' or 'TENANT_ADMIN' authority.";
  52 + protected static final String TENANT_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' authority.";
  53 + protected static final String TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'TENANT_ADMIN' or 'CUSTOMER_USER' authority.";
  54 + protected static final String CUSTOMER_AUTHORITY_PARAGRAPH = "\n\nAvailable for users with 'CUSTOMER_USER' authority.";
  55 + protected static final String AVAILABLE_FOR_ANY_AUTHORIZED_USER = "\n\nAvailable for any authorized user. ";
  56 + protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page";
  57 + protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0";
  58 + protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile";
  59 + protected static final String ASSET_TYPE_DESCRIPTION = "Asset type";
  60 + protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'";
  61 + protected static final String RULE_CHAIN_TYPE_DESCRIPTION = "Rule chain type (CORE or EDGE)";
  62 + protected static final String ASSET_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the asset name.";
  63 + protected static final String DASHBOARD_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the dashboard title.";
  64 + protected static final String WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the widget bundle title.";
  65 + protected static final String RPC_TEXT_SEARCH_DESCRIPTION = "Not implemented. Leave empty.";
  66 + protected static final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device name.";
  67 + protected static final String USER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the user email.";
  68 + protected static final String TENANT_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant name.";
  69 + protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant profile name.";
  70 + protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the rule chain name.";
  71 + protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device profile name.";
  72 + protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the customer title.";
  73 + protected static final String EDGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the edge name.";
  74 + protected static final String EVENT_TEXT_SEARCH_DESCRIPTION = "The value is not used in searching.";
  75 + 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.";
  76 + protected static final String SORT_PROPERTY_DESCRIPTION = "Property of entity to sort by";
  77 + protected static final String DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title";
  78 + protected static final String CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, city";
  79 + protected static final String RPC_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, expirationTime, request, response";
  80 + protected static final String DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deviceProfileName, label, customerTitle";
  81 + protected static final String USER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, firstName, lastName, email";
  82 + protected static final String TENANT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, state, city, address, address2, zip, phone, email";
  83 + protected static final String TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault";
  84 + protected static final String TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";
  85 + protected static final String TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, tenantProfileName, title, email, country, state, city, address, address2, zip, phone, email";
  86 + protected static final String DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, transportType, description, isDefault";
  87 + protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
  88 + protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status";
  89 + protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, id";
  90 + protected static final String EDGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
  91 + protected static final String RULE_CHAIN_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, root";
  92 + protected static final String WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, tenantId";
  93 + protected static final String AUDIT_LOG_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, entityType, entityName, userName, actionType, actionStatus";
  94 + protected static final String SORT_ORDER_DESCRIPTION = "Sort order. ASC (ASCENDING) or DESC (DESCENDING)";
  95 + protected static final String SORT_ORDER_ALLOWABLE_VALUES = "ASC, DESC";
  96 + protected static final String RPC_STATUS_ALLOWABLE_VALUES = "QUEUED, SENT, DELIVERED, SUCCESSFUL, TIMEOUT, EXPIRED, FAILED";
  97 + protected static final String RULE_CHAIN_TYPES_ALLOWABLE_VALUES = "CORE, EDGE";
  98 + protected static final String TRANSPORT_TYPE_ALLOWABLE_VALUES = "DEFAULT, MQTT, COAP, LWM2M, SNMP";
  99 + 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. ";
  100 + 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. ";
  101 + 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.";
  102 + 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. ";
  103 + 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. ";
  104 + 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. ";
  105 + protected static final String QUEUE_SERVICE_TYPE_DESCRIPTION = "Service type (implemented only for the TB-RULE-ENGINE)";
  106 + protected static final String QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES = "TB-RULE-ENGINE, TB-CORE, TB-TRANSPORT, JS-EXECUTOR";
  107 + 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. ";
  108 + protected static final String OTA_PACKAGE_DESCRIPTION = "OTA Package is a heavyweight object that includes main information about the OTA Package and also data. ";
  109 + protected static final String OTA_PACKAGE_CHECKSUM_ALGORITHM_ALLOWABLE_VALUES = "MD5, SHA256, SHA384, SHA512, CRC32, MURMUR3_32, MURMUR3_128";
  110 + protected static final String OTA_PACKAGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the ota package title.";
  111 + protected static final String OTA_PACKAGE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, type, title, version, tag, url, fileName, dataSize, checksum";
  112 + protected static final String RESOURCE_INFO_DESCRIPTION = "Resource Info is a lightweight object that includes main information about the Resource excluding the heavyweight data. ";
  113 + protected static final String RESOURCE_DESCRIPTION = "Resource is a heavyweight object that includes main information about the Resource and also data. ";
  114 +
  115 + protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the resource title.";
  116 + protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId";
  117 + 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. ";
  118 + protected static final String LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";
  119 +
  120 + protected static final String DEVICE_NAME_DESCRIPTION = "A string value representing the Device name.";
  121 + protected static final String ASSET_NAME_DESCRIPTION = "A string value representing the Asset name.";
  122 +
  123 + protected static final String EVENT_START_TIME_DESCRIPTION = "Timestamp. Events with creation time before it won't be queried.";
  124 + protected static final String EVENT_END_TIME_DESCRIPTION = "Timestamp. Events with creation time after it won't be queried.";
  125 +
  126 + 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. ";
  127 + 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)";
  128 + 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. ";
  129 + 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)";
  130 +
  131 + protected static final String MARKDOWN_CODE_BLOCK_START = "```json\n";
  132 + protected static final String MARKDOWN_CODE_BLOCK_END = "\n```";
  133 + protected static final String EVENT_ERROR_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
  134 + "{\n" +
  135 + " \"eventType\":\"ERROR\",\n" +
  136 + " \"server\":\"ip-172-31-24-152\",\n" +
  137 + " \"method\":\"onClusterEventMsg\",\n" +
  138 + " \"errorStr\":\"Error Message\"\n" +
  139 + "}" +
  140 + MARKDOWN_CODE_BLOCK_END;
  141 + protected static final String EVENT_LC_EVENT_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
  142 + "{\n" +
  143 + " \"eventType\":\"LC_EVENT\",\n" +
  144 + " \"server\":\"ip-172-31-24-152\",\n" +
  145 + " \"event\":\"STARTED\",\n" +
  146 + " \"status\":\"Success\",\n" +
  147 + " \"errorStr\":\"Error Message\"\n" +
  148 + "}" +
  149 + MARKDOWN_CODE_BLOCK_END;
  150 + protected static final String EVENT_STATS_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START +
  151 + "{\n" +
  152 + " \"eventType\":\"STATS\",\n" +
  153 + " \"server\":\"ip-172-31-24-152\",\n" +
  154 + " \"messagesProcessed\":10,\n" +
  155 + " \"errorsOccurred\":5\n" +
  156 + "}" +
  157 + MARKDOWN_CODE_BLOCK_END;
  158 + protected static final String DEBUG_FILTER_OBJ =
  159 + " \"msgDirectionType\":\"IN\",\n" +
  160 + " \"server\":\"ip-172-31-24-152\",\n" +
  161 + " \"dataSearch\":\"humidity\",\n" +
  162 + " \"metadataSearch\":\"deviceName\",\n" +
  163 + " \"entityName\":\"DEVICE\",\n" +
  164 + " \"relationType\":\"Success\",\n" +
  165 + " \"entityId\":\"de9d54a0-2b7a-11ec-a3cc-23386423d98f\",\n" +
  166 + " \"msgType\":\"POST_TELEMETRY_REQUEST\",\n" +
  167 + " \"isError\":\"false\",\n" +
  168 + " \"errorStr\":\"Error Message\"\n" +
  169 + "}";
  170 + protected static final String EVENT_DEBUG_RULE_NODE_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
  171 + " \"eventType\":\"DEBUG_RULE_NODE\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END;
  172 + protected static final String EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
  173 + " \"eventType\":\"DEBUG_RULE_CHAIN\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END;
  174 +
  175 + protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
  176 + "Provides a hint about the data type of the entity field that is defined in the filter key. " +
  177 + "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." +
  178 + "The following filter value types and corresponding predicate operations are supported: " + NEW_LINE +
  179 + " * 'STRING' - used to filter any 'String' or 'JSON' values. Operations: EQUAL, NOT_EQUAL, STARTS_WITH, ENDS_WITH, CONTAINS, NOT_CONTAINS; \n" +
  180 + " * 'NUMERIC' - used for 'Long' and 'Double' values. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n" +
  181 + " * 'BOOLEAN' - used for boolean values. Operations: EQUAL, NOT_EQUAL;\n" +
  182 + " * 'DATE_TIME' - similar to numeric, transforms value to milliseconds since epoch. Operations: EQUAL, NOT_EQUAL, GREATER, LESS, GREATER_OR_EQUAL, LESS_OR_EQUAL; \n";
  183 +
  184 + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
  185 + "{\n" +
  186 + " \"schedule\":{\n" +
  187 + " \"type\":\"SPECIFIC_TIME\",\n" +
  188 + " \"endsOn\":64800000,\n" +
  189 + " \"startsOn\":43200000,\n" +
  190 + " \"timezone\":\"Europe/Kiev\",\n" +
  191 + " \"daysOfWeek\":[\n" +
  192 + " 1,\n" +
  193 + " 3,\n" +
  194 + " 5\n" +
  195 + " ]\n" +
  196 + " }\n" +
  197 + "}" +
  198 + MARKDOWN_CODE_BLOCK_END;
  199 + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
  200 + "{\n" +
  201 + " \"schedule\":{\n" +
  202 + " \"type\":\"CUSTOM\",\n" +
  203 + " \"items\":[\n" +
  204 + " {\n" +
  205 + " \"endsOn\":0,\n" +
  206 + " \"enabled\":false,\n" +
  207 + " \"startsOn\":0,\n" +
  208 + " \"dayOfWeek\":1\n" +
  209 + " },\n" +
  210 + " {\n" +
  211 + " \"endsOn\":64800000,\n" +
  212 + " \"enabled\":true,\n" +
  213 + " \"startsOn\":43200000,\n" +
  214 + " \"dayOfWeek\":2\n" +
  215 + " },\n" +
  216 + " {\n" +
  217 + " \"endsOn\":0,\n" +
  218 + " \"enabled\":false,\n" +
  219 + " \"startsOn\":0,\n" +
  220 + " \"dayOfWeek\":3\n" +
  221 + " },\n" +
  222 + " {\n" +
  223 + " \"endsOn\":57600000,\n" +
  224 + " \"enabled\":true,\n" +
  225 + " \"startsOn\":36000000,\n" +
  226 + " \"dayOfWeek\":4\n" +
  227 + " },\n" +
  228 + " {\n" +
  229 + " \"endsOn\":0,\n" +
  230 + " \"enabled\":false,\n" +
  231 + " \"startsOn\":0,\n" +
  232 + " \"dayOfWeek\":5\n" +
  233 + " },\n" +
  234 + " {\n" +
  235 + " \"endsOn\":0,\n" +
  236 + " \"enabled\":false,\n" +
  237 + " \"startsOn\":0,\n" +
  238 + " \"dayOfWeek\":6\n" +
  239 + " },\n" +
  240 + " {\n" +
  241 + " \"endsOn\":0,\n" +
  242 + " \"enabled\":false,\n" +
  243 + " \"startsOn\":0,\n" +
  244 + " \"dayOfWeek\":7\n" +
  245 + " }\n" +
  246 + " ],\n" +
  247 + " \"timezone\":\"Europe/Kiev\"\n" +
  248 + " }\n" +
  249 + "}" +
  250 + MARKDOWN_CODE_BLOCK_END;
  251 + protected static final String DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "\"schedule\": null" + MARKDOWN_CODE_BLOCK_END;
  252 +
  253 + protected static final String DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
  254 + "{\n" +
  255 + " \"spec\":{\n" +
  256 + " \"type\":\"REPEATING\",\n" +
  257 + " \"predicate\":{\n" +
  258 + " \"userValue\":null,\n" +
  259 + " \"defaultValue\":5,\n" +
  260 + " \"dynamicValue\":{\n" +
  261 + " \"inherit\":true,\n" +
  262 + " \"sourceType\":\"CURRENT_DEVICE\",\n" +
  263 + " \"sourceAttribute\":\"tempAttr\"\n" +
  264 + " }\n" +
  265 + " }\n" +
  266 + " }\n" +
  267 + "}" +
  268 + MARKDOWN_CODE_BLOCK_END;
  269 + protected static final String DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
  270 + "{\n" +
  271 + " \"spec\":{\n" +
  272 + " \"type\":\"DURATION\",\n" +
  273 + " \"unit\":\"MINUTES\",\n" +
  274 + " \"predicate\":{\n" +
  275 + " \"userValue\":null,\n" +
  276 + " \"defaultValue\":30,\n" +
  277 + " \"dynamicValue\":null\n" +
  278 + " }\n" +
  279 + " }\n" +
  280 + "}" +
  281 + MARKDOWN_CODE_BLOCK_END;
  282 +
  283 + 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.";
  284 + protected static final String RELATION_TYPE_GROUP_PARAM_DESCRIPTION = "A string value representing relation type group. For example, 'COMMON'";
  285 +
  286 + public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
  287 + protected static final String DEFAULT_DASHBOARD = "defaultDashboardId";
  288 + protected static final String HOME_DASHBOARD = "homeDashboardId";
  289 +
  290 + protected static final String SINGLE_ENTITY = "\n\n## Single Entity\n\n" +
  291 + "Allows to filter only one entity based on the id. For example, this entity filter selects certain device:\n\n" +
  292 + MARKDOWN_CODE_BLOCK_START +
  293 + "{\n" +
  294 + " \"type\": \"singleEntity\",\n" +
  295 + " \"singleEntity\": {\n" +
  296 + " \"id\": \"d521edb0-2a7a-11ec-94eb-213c95f54092\",\n" +
  297 + " \"entityType\": \"DEVICE\"\n" +
  298 + " }\n" +
  299 + "}" +
  300 + MARKDOWN_CODE_BLOCK_END +
  301 + "";
  302 +
  303 + protected static final String ENTITY_LIST = "\n\n## Entity List Filter\n\n" +
  304 + "Allows to filter entities of the same type using their ids. For example, this entity filter selects two devices:\n\n" +
  305 + MARKDOWN_CODE_BLOCK_START +
  306 + "{\n" +
  307 + " \"type\": \"entityList\",\n" +
  308 + " \"entityType\": \"DEVICE\",\n" +
  309 + " \"entityList\": [\n" +
  310 + " \"e6501f30-2a7a-11ec-94eb-213c95f54092\",\n" +
  311 + " \"e6657bf0-2a7a-11ec-94eb-213c95f54092\"\n" +
  312 + " ]\n" +
  313 + "}" +
  314 + MARKDOWN_CODE_BLOCK_END +
  315 + "";
  316 +
  317 + protected static final String ENTITY_NAME = "\n\n## Entity Name Filter\n\n" +
  318 + "Allows to filter entities of the same type using the **'starts with'** expression over entity name. " +
  319 + "For example, this entity filter selects all devices which name starts with 'Air Quality':\n\n" +
  320 + MARKDOWN_CODE_BLOCK_START +
  321 + "{\n" +
  322 + " \"type\": \"entityName\",\n" +
  323 + " \"entityType\": \"DEVICE\",\n" +
  324 + " \"entityNameFilter\": \"Air Quality\"\n" +
  325 + "}" +
  326 + MARKDOWN_CODE_BLOCK_END +
  327 + "";
  328 +
  329 + protected static final String ENTITY_TYPE_FILTER = "\n\n## Entity Type Filter\n\n" +
  330 + "Allows to filter entities based on their type (CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, etc)" +
  331 + "For example, this entity filter selects all tenant customers:\n\n" +
  332 + MARKDOWN_CODE_BLOCK_START +
  333 + "{\n" +
  334 + " \"type\": \"entityType\",\n" +
  335 + " \"entityType\": \"CUSTOMER\"\n" +
  336 + "}" +
  337 + MARKDOWN_CODE_BLOCK_END +
  338 + "";
  339 +
  340 + protected static final String ASSET_TYPE = "\n\n## Asset Type Filter\n\n" +
  341 + "Allows to filter assets based on their type and the **'starts with'** expression over their name. " +
  342 + "For example, this entity filter selects all 'charging station' assets which name starts with 'Tesla':\n\n" +
  343 + MARKDOWN_CODE_BLOCK_START +
  344 + "{\n" +
  345 + " \"type\": \"assetType\",\n" +
  346 + " \"assetType\": \"charging station\",\n" +
  347 + " \"assetNameFilter\": \"Tesla\"\n" +
  348 + "}" +
  349 + MARKDOWN_CODE_BLOCK_END +
  350 + "";
  351 +
  352 + protected static final String DEVICE_TYPE = "\n\n## Device Type Filter\n\n" +
  353 + "Allows to filter devices based on their type and the **'starts with'** expression over their name. " +
  354 + "For example, this entity filter selects all 'Temperature Sensor' devices which name starts with 'ABC':\n\n" +
  355 + MARKDOWN_CODE_BLOCK_START +
  356 + "{\n" +
  357 + " \"type\": \"deviceType\",\n" +
  358 + " \"deviceType\": \"Temperature Sensor\",\n" +
  359 + " \"deviceNameFilter\": \"ABC\"\n" +
  360 + "}" +
  361 + MARKDOWN_CODE_BLOCK_END +
  362 + "";
  363 +
  364 + protected static final String EDGE_TYPE = "\n\n## Edge Type Filter\n\n" +
  365 + "Allows to filter edge instances based on their type and the **'starts with'** expression over their name. " +
  366 + "For example, this entity filter selects all 'Factory' edge instances which name starts with 'Nevada':\n\n" +
  367 + MARKDOWN_CODE_BLOCK_START +
  368 + "{\n" +
  369 + " \"type\": \"edgeType\",\n" +
  370 + " \"edgeType\": \"Factory\",\n" +
  371 + " \"edgeNameFilter\": \"Nevada\"\n" +
  372 + "}" +
  373 + MARKDOWN_CODE_BLOCK_END +
  374 + "";
  375 +
  376 + protected static final String ENTITY_VIEW_TYPE = "\n\n## Entity View Filter\n\n" +
  377 + "Allows to filter entity views based on their type and the **'starts with'** expression over their name. " +
  378 + "For example, this entity filter selects all 'Concrete Mixer' entity views which name starts with 'CAT':\n\n" +
  379 + MARKDOWN_CODE_BLOCK_START +
  380 + "{\n" +
  381 + " \"type\": \"entityViewType\",\n" +
  382 + " \"entityViewType\": \"Concrete Mixer\",\n" +
  383 + " \"entityViewNameFilter\": \"CAT\"\n" +
  384 + "}" +
  385 + MARKDOWN_CODE_BLOCK_END +
  386 + "";
  387 +
  388 + protected static final String API_USAGE = "\n\n## Api Usage Filter\n\n" +
  389 + "Allows to query for Api Usage based on optional customer id. If the customer id is not set, returns current tenant API usage." +
  390 + "For example, this entity filter selects the 'Api Usage' entity for customer with id 'e6501f30-2a7a-11ec-94eb-213c95f54092':\n\n" +
  391 + MARKDOWN_CODE_BLOCK_START +
  392 + "{\n" +
  393 + " \"type\": \"apiUsageState\",\n" +
  394 + " \"customerId\": {\n" +
  395 + " \"id\": \"d521edb0-2a7a-11ec-94eb-213c95f54092\",\n" +
  396 + " \"entityType\": \"CUSTOMER\"\n" +
  397 + " }\n" +
  398 + "}" +
  399 + MARKDOWN_CODE_BLOCK_END +
  400 + "";
  401 +
  402 + 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'. ";
  403 + 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. ";
  404 +
  405 + protected static final String RELATIONS_QUERY_FILTER = "\n\n## Relations Query Filter\n\n" +
  406 + "Allows to filter entities that are related to the provided root entity. " +
  407 + MAX_LEVEL_DESCRIPTION +
  408 + FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
  409 + "The 'filter' object allows you to define the relation type and set of acceptable entity types to search for. " +
  410 + "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" +
  411 + "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" +
  412 + MARKDOWN_CODE_BLOCK_START +
  413 + "{\n" +
  414 + " \"type\": \"relationsQuery\",\n" +
  415 + " \"rootEntity\": {\n" +
  416 + " \"entityType\": \"ASSET\",\n" +
  417 + " \"id\": \"e51de0c0-2a7a-11ec-94eb-213c95f54092\"\n" +
  418 + " },\n" +
  419 + " \"direction\": \"FROM\",\n" +
  420 + " \"maxLevel\": 1,\n" +
  421 + " \"fetchLastLevelOnly\": false,\n" +
  422 + " \"filters\": [\n" +
  423 + " {\n" +
  424 + " \"relationType\": \"Contains\",\n" +
  425 + " \"entityTypes\": [\n" +
  426 + " \"DEVICE\",\n" +
  427 + " \"ASSET\"\n" +
  428 + " ]\n" +
  429 + " }\n" +
  430 + " ]\n" +
  431 + "}" +
  432 + MARKDOWN_CODE_BLOCK_END +
  433 + "";
  434 +
  435 +
  436 + protected static final String ASSET_QUERY_FILTER = "\n\n## Asset Search Query\n\n" +
  437 + "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. " +
  438 + MAX_LEVEL_DESCRIPTION +
  439 + FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
  440 + "The 'relationType' defines the type of the relation to search for. " +
  441 + "The 'assetTypes' defines the type of the asset to search for. " +
  442 + "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" +
  443 + "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" +
  444 + MARKDOWN_CODE_BLOCK_START +
  445 + "{\n" +
  446 + " \"type\": \"assetSearchQuery\",\n" +
  447 + " \"rootEntity\": {\n" +
  448 + " \"entityType\": \"ASSET\",\n" +
  449 + " \"id\": \"e51de0c0-2a7a-11ec-94eb-213c95f54092\"\n" +
  450 + " },\n" +
  451 + " \"direction\": \"FROM\",\n" +
  452 + " \"maxLevel\": 1,\n" +
  453 + " \"fetchLastLevelOnly\": false,\n" +
  454 + " \"relationType\": \"Contains\",\n" +
  455 + " \"assetTypes\": [\n" +
  456 + " \"charging station\"\n" +
  457 + " ]\n" +
  458 + "}" +
  459 + MARKDOWN_CODE_BLOCK_END +
  460 + "";
  461 +
  462 + protected static final String DEVICE_QUERY_FILTER = "\n\n## Device Search Query\n\n" +
  463 + "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. " +
  464 + MAX_LEVEL_DESCRIPTION +
  465 + FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
  466 + "The 'relationType' defines the type of the relation to search for. " +
  467 + "The 'deviceTypes' defines the type of the device to search for. " +
  468 + "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" +
  469 + "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" +
  470 + MARKDOWN_CODE_BLOCK_START +
  471 + "{\n" +
  472 + " \"type\": \"deviceSearchQuery\",\n" +
  473 + " \"rootEntity\": {\n" +
  474 + " \"entityType\": \"ASSET\",\n" +
  475 + " \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" +
  476 + " },\n" +
  477 + " \"direction\": \"FROM\",\n" +
  478 + " \"maxLevel\": 2,\n" +
  479 + " \"fetchLastLevelOnly\": true,\n" +
  480 + " \"relationType\": \"Contains\",\n" +
  481 + " \"deviceTypes\": [\n" +
  482 + " \"Air Quality Sensor\",\n" +
  483 + " \"Charging port\"\n" +
  484 + " ]\n" +
  485 + "}" +
  486 + MARKDOWN_CODE_BLOCK_END +
  487 + "";
  488 +
  489 + protected static final String EV_QUERY_FILTER = "\n\n## Entity View Query\n\n" +
  490 + "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. " +
  491 + MAX_LEVEL_DESCRIPTION +
  492 + FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
  493 + "The 'relationType' defines the type of the relation to search for. " +
  494 + "The 'entityViewTypes' defines the type of the entity view to search for. " +
  495 + "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" +
  496 + "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" +
  497 + MARKDOWN_CODE_BLOCK_START +
  498 + "{\n" +
  499 + " \"type\": \"entityViewSearchQuery\",\n" +
  500 + " \"rootEntity\": {\n" +
  501 + " \"entityType\": \"ASSET\",\n" +
  502 + " \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" +
  503 + " },\n" +
  504 + " \"direction\": \"FROM\",\n" +
  505 + " \"maxLevel\": 1,\n" +
  506 + " \"fetchLastLevelOnly\": false,\n" +
  507 + " \"relationType\": \"Contains\",\n" +
  508 + " \"entityViewTypes\": [\n" +
  509 + " \"Concrete mixer\"\n" +
  510 + " ]\n" +
  511 + "}" +
  512 + MARKDOWN_CODE_BLOCK_END +
  513 + "";
  514 +
  515 + protected static final String EDGE_QUERY_FILTER = "\n\n## Edge Search Query\n\n" +
  516 + "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. " +
  517 + MAX_LEVEL_DESCRIPTION +
  518 + FETCH_LAST_LEVEL_ONLY_DESCRIPTION +
  519 + "The 'relationType' defines the type of the relation to search for. " +
  520 + "The 'deviceTypes' defines the type of the device to search for. " +
  521 + "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" +
  522 + "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" +
  523 + MARKDOWN_CODE_BLOCK_START +
  524 + "{\n" +
  525 + " \"type\": \"deviceSearchQuery\",\n" +
  526 + " \"rootEntity\": {\n" +
  527 + " \"entityType\": \"ASSET\",\n" +
  528 + " \"id\": \"e52b0020-2a7a-11ec-94eb-213c95f54092\"\n" +
  529 + " },\n" +
  530 + " \"direction\": \"FROM\",\n" +
  531 + " \"maxLevel\": 2,\n" +
  532 + " \"fetchLastLevelOnly\": true,\n" +
  533 + " \"relationType\": \"Contains\",\n" +
  534 + " \"edgeTypes\": [\n" +
  535 + " \"Factory\"\n" +
  536 + " ]\n" +
  537 + "}" +
  538 + MARKDOWN_CODE_BLOCK_END +
  539 + "";
  540 +
  541 + protected static final String EMPTY = "\n\n## Entity Type Filter\n\n" +
  542 + "Allows to filter multiple entities of the same type using the **'starts with'** expression over entity name. " +
  543 + "For example, this entity filter selects all devices which name starts with 'Air Quality':\n\n" +
  544 + MARKDOWN_CODE_BLOCK_START +
  545 + "" +
  546 + MARKDOWN_CODE_BLOCK_END +
  547 + "";
  548 +
  549 + protected static final String ENTITY_FILTERS =
  550 + "\n\n # Entity Filters" +
  551 + "\nEntity Filter body depends on the 'type' parameter. Let's review available entity filter types. In fact, they do correspond to available dashboard aliases." +
  552 + SINGLE_ENTITY + ENTITY_LIST + ENTITY_NAME + ENTITY_TYPE_FILTER + ASSET_TYPE + DEVICE_TYPE + EDGE_TYPE + ENTITY_VIEW_TYPE + API_USAGE + RELATIONS_QUERY_FILTER
  553 + + ASSET_QUERY_FILTER + DEVICE_QUERY_FILTER + EV_QUERY_FILTER + EDGE_QUERY_FILTER;
  554 +
  555 + protected static final String FILTER_KEY = "\n\n## Filter Key\n\n" +
  556 + "Filter Key defines either entity field, attribute or telemetry. It is a JSON object that consists the key name and type. " +
  557 + "The following filter key types are supported: \n\n" +
  558 + " * 'CLIENT_ATTRIBUTE' - used for client attributes; \n" +
  559 + " * 'SHARED_ATTRIBUTE' - used for shared attributes; \n" +
  560 + " * 'SERVER_ATTRIBUTE' - used for server attributes; \n" +
  561 + " * 'ATTRIBUTE' - used for any of the above; \n" +
  562 + " * 'TIME_SERIES' - used for time-series values; \n" +
  563 + " * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type; \n" +
  564 + " * 'ALARM_FIELD' - similar to entity field, but is used in alarm queries only; \n" +
  565 + "\n\n Let's review the example:\n\n" +
  566 + MARKDOWN_CODE_BLOCK_START +
  567 + "{\n" +
  568 + " \"type\": \"TIME_SERIES\",\n" +
  569 + " \"key\": \"temperature\"\n" +
  570 + "}" +
  571 + MARKDOWN_CODE_BLOCK_END +
  572 + "";
  573 +
  574 + protected static final String FILTER_PREDICATE = "\n\n## Filter Predicate\n\n" +
  575 + "Filter Predicate defines the logical expression to evaluate. The list of available operations depends on the filter value type, see above. " +
  576 + "Platform supports 4 predicate types: 'STRING', 'NUMERIC', 'BOOLEAN' and 'COMPLEX'. The last one allows to combine multiple operations over one filter key." +
  577 + "\n\nSimple predicate example to check 'value < 100': \n\n" +
  578 + MARKDOWN_CODE_BLOCK_START +
  579 + "{\n" +
  580 + " \"operation\": \"LESS\",\n" +
  581 + " \"value\": {\n" +
  582 + " \"defaultValue\": 100,\n" +
  583 + " \"dynamicValue\": null\n" +
  584 + " },\n" +
  585 + " \"type\": \"NUMERIC\"\n" +
  586 + "}" +
  587 + MARKDOWN_CODE_BLOCK_END +
  588 + "\n\nComplex predicate example, to check 'value < 10 or value > 20': \n\n" +
  589 + MARKDOWN_CODE_BLOCK_START +
  590 + "{\n" +
  591 + " \"type\": \"COMPLEX\",\n" +
  592 + " \"operation\": \"OR\",\n" +
  593 + " \"predicates\": [\n" +
  594 + " {\n" +
  595 + " \"operation\": \"LESS\",\n" +
  596 + " \"value\": {\n" +
  597 + " \"defaultValue\": 10,\n" +
  598 + " \"dynamicValue\": null\n" +
  599 + " },\n" +
  600 + " \"type\": \"NUMERIC\"\n" +
  601 + " },\n" +
  602 + " {\n" +
  603 + " \"operation\": \"GREATER\",\n" +
  604 + " \"value\": {\n" +
  605 + " \"defaultValue\": 20,\n" +
  606 + " \"dynamicValue\": null\n" +
  607 + " },\n" +
  608 + " \"type\": \"NUMERIC\"\n" +
  609 + " }\n" +
  610 + " ]\n" +
  611 + "}" +
  612 + MARKDOWN_CODE_BLOCK_END +
  613 + "\n\nMore complex predicate example, to check 'value < 10 or (value > 50 && value < 60)': \n\n" +
  614 + MARKDOWN_CODE_BLOCK_START +
  615 + "{\n" +
  616 + " \"type\": \"COMPLEX\",\n" +
  617 + " \"operation\": \"OR\",\n" +
  618 + " \"predicates\": [\n" +
  619 + " {\n" +
  620 + " \"operation\": \"LESS\",\n" +
  621 + " \"value\": {\n" +
  622 + " \"defaultValue\": 10,\n" +
  623 + " \"dynamicValue\": null\n" +
  624 + " },\n" +
  625 + " \"type\": \"NUMERIC\"\n" +
  626 + " },\n" +
  627 + " {\n" +
  628 + " \"type\": \"COMPLEX\",\n" +
  629 + " \"operation\": \"AND\",\n" +
  630 + " \"predicates\": [\n" +
  631 + " {\n" +
  632 + " \"operation\": \"GREATER\",\n" +
  633 + " \"value\": {\n" +
  634 + " \"defaultValue\": 50,\n" +
  635 + " \"dynamicValue\": null\n" +
  636 + " },\n" +
  637 + " \"type\": \"NUMERIC\"\n" +
  638 + " },\n" +
  639 + " {\n" +
  640 + " \"operation\": \"LESS\",\n" +
  641 + " \"value\": {\n" +
  642 + " \"defaultValue\": 60,\n" +
  643 + " \"dynamicValue\": null\n" +
  644 + " },\n" +
  645 + " \"type\": \"NUMERIC\"\n" +
  646 + " }\n" +
  647 + " ]\n" +
  648 + " }\n" +
  649 + " ]\n" +
  650 + "}" +
  651 + MARKDOWN_CODE_BLOCK_END +
  652 + "\n\n You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " +
  653 + "expression (for example, temperature > 'value of the tenant attribute with key 'temperatureThreshold'). " +
  654 + "It is possible to use 'dynamicValue' to define attribute of the tenant, customer or user that is performing the API call. " +
  655 + "See example below: \n\n" +
  656 + MARKDOWN_CODE_BLOCK_START +
  657 + "{\n" +
  658 + " \"operation\": \"GREATER\",\n" +
  659 + " \"value\": {\n" +
  660 + " \"defaultValue\": 0,\n" +
  661 + " \"dynamicValue\": {\n" +
  662 + " \"sourceType\": \"CURRENT_USER\",\n" +
  663 + " \"sourceAttribute\": \"temperatureThreshold\"\n" +
  664 + " }\n" +
  665 + " },\n" +
  666 + " \"type\": \"NUMERIC\"\n" +
  667 + "}" +
  668 + MARKDOWN_CODE_BLOCK_END +
  669 + "\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.";
  670 +
  671 + protected static final String KEY_FILTERS =
  672 + "\n\n # Key Filters" +
  673 + "\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. " +
  674 + "Single Entity Query may have zero, one or multiple predicates. If multiple filters are defined, they are evaluated using logical 'AND'. " +
  675 + "The example below checks that temperature of the entity is above 20 degrees:" +
  676 + "\n\n" + MARKDOWN_CODE_BLOCK_START +
  677 + "{\n" +
  678 + " \"key\": {\n" +
  679 + " \"type\": \"TIME_SERIES\",\n" +
  680 + " \"key\": \"temperature\"\n" +
  681 + " },\n" +
  682 + " \"valueType\": \"NUMERIC\",\n" +
  683 + " \"predicate\": {\n" +
  684 + " \"operation\": \"GREATER\",\n" +
  685 + " \"value\": {\n" +
  686 + " \"defaultValue\": 20,\n" +
  687 + " \"dynamicValue\": null\n" +
  688 + " },\n" +
  689 + " \"type\": \"NUMERIC\"\n" +
  690 + " }\n" +
  691 + "}" +
  692 + MARKDOWN_CODE_BLOCK_END +
  693 + "\n\n Now let's review 'key', 'valueType' and 'predicate' objects in detail."
  694 + + FILTER_KEY + FILTER_VALUE_TYPE + FILTER_PREDICATE;
  695 +
  696 + protected static final String ENTITY_COUNT_QUERY_DESCRIPTION =
  697 + "Allows to run complex queries to search the count of platform entities (devices, assets, customers, etc) " +
  698 + "based on the combination of main entity filter and multiple key filters. Returns the number of entities that match the query definition.\n\n" +
  699 + "# Query Definition\n\n" +
  700 + "\n\nMain **entity filter** is mandatory and defines generic search criteria. " +
  701 + "For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" +
  702 + "\n\nOptional **key filters** allow to filter results of the entity filter by complex criteria against " +
  703 + "main entity fields (name, label, type, etc), attributes and telemetry. " +
  704 + "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." +
  705 + "\n\nLet's review the example:" +
  706 + "\n\n" + MARKDOWN_CODE_BLOCK_START +
  707 + "{\n" +
  708 + " \"entityFilter\": {\n" +
  709 + " \"type\": \"entityType\",\n" +
  710 + " \"entityType\": \"DEVICE\"\n" +
  711 + " },\n" +
  712 + " \"keyFilters\": [\n" +
  713 + " {\n" +
  714 + " \"key\": {\n" +
  715 + " \"type\": \"ATTRIBUTE\",\n" +
  716 + " \"key\": \"active\"\n" +
  717 + " },\n" +
  718 + " \"valueType\": \"BOOLEAN\",\n" +
  719 + " \"predicate\": {\n" +
  720 + " \"operation\": \"EQUAL\",\n" +
  721 + " \"value\": {\n" +
  722 + " \"defaultValue\": true,\n" +
  723 + " \"dynamicValue\": null\n" +
  724 + " },\n" +
  725 + " \"type\": \"BOOLEAN\"\n" +
  726 + " }\n" +
  727 + " }\n" +
  728 + " ]\n" +
  729 + "}" +
  730 + MARKDOWN_CODE_BLOCK_END +
  731 + "\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:" +
  732 + ENTITY_FILTERS +
  733 + KEY_FILTERS +
  734 + ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  735 +
  736 + protected static final String ENTITY_DATA_QUERY_DESCRIPTION =
  737 + "Allows to run complex queries over platform entities (devices, assets, customers, etc) " +
  738 + "based on the combination of main entity filter and multiple key filters. " +
  739 + "Returns the paginated result of the query that contains requested entity fields and latest values of requested attributes and time-series data.\n\n" +
  740 + "# Query Definition\n\n" +
  741 + "\n\nMain **entity filter** is mandatory and defines generic search criteria. " +
  742 + "For example, \"find all devices with profile 'Moisture Sensor'\" or \"Find all devices related to asset 'Building A'\"" +
  743 + "\n\nOptional **key filters** allow to filter results of the **entity filter** by complex criteria against " +
  744 + "main entity fields (name, label, type, etc), attributes and telemetry. " +
  745 + "For example, \"temperature > 20 or temperature< 10\" or \"name starts with 'T', and attribute 'model' is 'T1000', and timeseries field 'batteryLevel' > 40\"." +
  746 + "\n\nThe **entity fields** and **latest values** contains list of entity fields and latest attribute/telemetry fields to fetch for each entity." +
  747 + "\n\nThe **page link** contains information about the page to fetch and the sort ordering." +
  748 + "\n\nLet's review the example:" +
  749 + "\n\n" + MARKDOWN_CODE_BLOCK_START +
  750 + "{\n" +
  751 + " \"entityFilter\": {\n" +
  752 + " \"type\": \"entityType\",\n" +
  753 + " \"resolveMultiple\": true,\n" +
  754 + " \"entityType\": \"DEVICE\"\n" +
  755 + " },\n" +
  756 + " \"keyFilters\": [\n" +
  757 + " {\n" +
  758 + " \"key\": {\n" +
  759 + " \"type\": \"TIME_SERIES\",\n" +
  760 + " \"key\": \"temperature\"\n" +
  761 + " },\n" +
  762 + " \"valueType\": \"NUMERIC\",\n" +
  763 + " \"predicate\": {\n" +
  764 + " \"operation\": \"GREATER\",\n" +
  765 + " \"value\": {\n" +
  766 + " \"defaultValue\": 0,\n" +
  767 + " \"dynamicValue\": {\n" +
  768 + " \"sourceType\": \"CURRENT_USER\",\n" +
  769 + " \"sourceAttribute\": \"temperatureThreshold\",\n" +
  770 + " \"inherit\": false\n" +
  771 + " }\n" +
  772 + " },\n" +
  773 + " \"type\": \"NUMERIC\"\n" +
  774 + " }\n" +
  775 + " }\n" +
  776 + " ],\n" +
  777 + " \"entityFields\": [\n" +
  778 + " {\n" +
  779 + " \"type\": \"ENTITY_FIELD\",\n" +
  780 + " \"key\": \"name\"\n" +
  781 + " },\n" +
  782 + " {\n" +
  783 + " \"type\": \"ENTITY_FIELD\",\n" +
  784 + " \"key\": \"label\"\n" +
  785 + " },\n" +
  786 + " {\n" +
  787 + " \"type\": \"ENTITY_FIELD\",\n" +
  788 + " \"key\": \"additionalInfo\"\n" +
  789 + " }\n" +
  790 + " ],\n" +
  791 + " \"latestValues\": [\n" +
  792 + " {\n" +
  793 + " \"type\": \"ATTRIBUTE\",\n" +
  794 + " \"key\": \"model\"\n" +
  795 + " },\n" +
  796 + " {\n" +
  797 + " \"type\": \"TIME_SERIES\",\n" +
  798 + " \"key\": \"temperature\"\n" +
  799 + " }\n" +
  800 + " ],\n" +
  801 + " \"pageLink\": {\n" +
  802 + " \"page\": 0,\n" +
  803 + " \"pageSize\": 10,\n" +
  804 + " \"sortOrder\": {\n" +
  805 + " \"key\": {\n" +
  806 + " \"key\": \"name\",\n" +
  807 + " \"type\": \"ENTITY_FIELD\"\n" +
  808 + " },\n" +
  809 + " \"direction\": \"ASC\"\n" +
  810 + " }\n" +
  811 + " }\n" +
  812 + "}" +
  813 + MARKDOWN_CODE_BLOCK_END +
  814 + "\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:" +
  815 + ENTITY_FILTERS +
  816 + KEY_FILTERS +
  817 + ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  818 +
  819 +
  820 + protected static final String ALARM_DATA_QUERY_DESCRIPTION = "This method description defines how Alarm Data Query extends the Entity Data Query. " +
  821 + "See method 'Find Entity Data by Query' first to get the info about 'Entity Data Query'." +
  822 + "\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. " +
  823 + "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. " +
  824 + "\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. " +
  825 + "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: " +
  826 + "\n\n" + MARKDOWN_CODE_BLOCK_START +
  827 + "{\n" +
  828 + " \"entityFilter\": {\n" +
  829 + " \"type\": \"entityType\",\n" +
  830 + " \"resolveMultiple\": true,\n" +
  831 + " \"entityType\": \"DEVICE\"\n" +
  832 + " },\n" +
  833 + " \"pageLink\": {\n" +
  834 + " \"page\": 0,\n" +
  835 + " \"pageSize\": 100,\n" +
  836 + " \"textSearch\": null,\n" +
  837 + " \"searchPropagatedAlarms\": false,\n" +
  838 + " \"statusList\": [\n" +
  839 + " \"ACTIVE\"\n" +
  840 + " ],\n" +
  841 + " \"severityList\": [\n" +
  842 + " \"CRITICAL\",\n" +
  843 + " \"MAJOR\"\n" +
  844 + " ],\n" +
  845 + " \"typeList\": [\n" +
  846 + " \"Temperature Alarm\",\n" +
  847 + " \"Fire Alarm\"\n" +
  848 + " ],\n" +
  849 + " \"sortOrder\": {\n" +
  850 + " \"key\": {\n" +
  851 + " \"key\": \"createdTime\",\n" +
  852 + " \"type\": \"ALARM_FIELD\"\n" +
  853 + " },\n" +
  854 + " \"direction\": \"DESC\"\n" +
  855 + " },\n" +
  856 + " \"timeWindow\": 86400000\n" +
  857 + " },\n" +
  858 + " \"keyFilters\": [\n" +
  859 + " {\n" +
  860 + " \"key\": {\n" +
  861 + " \"type\": \"TIME_SERIES\",\n" +
  862 + " \"key\": \"temperature\"\n" +
  863 + " },\n" +
  864 + " \"valueType\": \"NUMERIC\",\n" +
  865 + " \"predicate\": {\n" +
  866 + " \"operation\": \"GREATER\",\n" +
  867 + " \"value\": {\n" +
  868 + " \"defaultValue\": 0,\n" +
  869 + " \"dynamicValue\": null\n" +
  870 + " },\n" +
  871 + " \"type\": \"NUMERIC\"\n" +
  872 + " }\n" +
  873 + " }\n" +
  874 + " ],\n" +
  875 + " \"alarmFields\": [\n" +
  876 + " {\n" +
  877 + " \"type\": \"ALARM_FIELD\",\n" +
  878 + " \"key\": \"createdTime\"\n" +
  879 + " },\n" +
  880 + " {\n" +
  881 + " \"type\": \"ALARM_FIELD\",\n" +
  882 + " \"key\": \"type\"\n" +
  883 + " },\n" +
  884 + " {\n" +
  885 + " \"type\": \"ALARM_FIELD\",\n" +
  886 + " \"key\": \"severity\"\n" +
  887 + " },\n" +
  888 + " {\n" +
  889 + " \"type\": \"ALARM_FIELD\",\n" +
  890 + " \"key\": \"status\"\n" +
  891 + " }\n" +
  892 + " ],\n" +
  893 + " \"entityFields\": [\n" +
  894 + " {\n" +
  895 + " \"type\": \"ENTITY_FIELD\",\n" +
  896 + " \"key\": \"name\"\n" +
  897 + " }\n" +
  898 + " ],\n" +
  899 + " \"latestValues\": [\n" +
  900 + " {\n" +
  901 + " \"type\": \"ATTRIBUTE\",\n" +
  902 + " \"key\": \"model\"\n" +
  903 + " },\n" +
  904 + " {\n" +
  905 + " \"type\": \"TIME_SERIES\",\n" +
  906 + " \"key\": \"temperature\"\n" +
  907 + " }\n" +
  908 + " ]\n" +
  909 + "}" +
  910 + MARKDOWN_CODE_BLOCK_END +
  911 + "";
  912 +
  913 + protected static final String COAP_TRANSPORT_CONFIGURATION_EXAMPLE = MARKDOWN_CODE_BLOCK_START +
  914 + "{\n" +
  915 + " \"type\":\"COAP\",\n" +
  916 + " \"clientSettings\":{\n" +
  917 + " \"edrxCycle\":null,\n" +
  918 + " \"powerMode\":\"DRX\",\n" +
  919 + " \"psmActivityTimer\":null,\n" +
  920 + " \"pagingTransmissionWindow\":null\n" +
  921 + " },\n" +
  922 + " \"coapDeviceTypeConfiguration\":{\n" +
  923 + " \"coapDeviceType\":\"DEFAULT\",\n" +
  924 + " \"transportPayloadTypeConfiguration\":{\n" +
  925 + " \"transportPayloadType\":\"JSON\"\n" +
  926 + " }\n" +
  927 + " }\n" +
  928 + "}"
  929 + + MARKDOWN_CODE_BLOCK_END;
  930 +
  931 + protected static final String TRANSPORT_CONFIGURATION = "# Transport Configuration" + NEW_LINE +
  932 + "5 transport configuration types are available:\n" +
  933 + " * 'DEFAULT';\n" +
  934 + " * 'MQTT';\n" +
  935 + " * 'LWM2M';\n" +
  936 + " * 'COAP';\n" +
  937 + " * 'SNMP'." + NEW_LINE + "Default type supports basic MQTT, HTTP, CoAP and LwM2M transports. " +
  938 + "Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-profiles/#transport-configuration) for more details about other types.\n" +
  939 + "\nSee another example of COAP transport configuration below:" + NEW_LINE + COAP_TRANSPORT_CONFIGURATION_EXAMPLE;
  940 +
  941 + protected static final String ALARM_FILTER_KEY = "## Alarm Filter Key" + NEW_LINE +
  942 + "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" +
  943 + " * 'ATTRIBUTE' - used for attributes values;\n" +
  944 + " * 'TIME_SERIES' - used for time-series values;\n" +
  945 + " * 'ENTITY_FIELD' - used for accessing entity fields like 'name', 'label', etc. The list of available fields depends on the entity type;\n" +
  946 + " * 'CONSTANT' - constant value specified." + NEW_LINE + "Let's review the example:" + NEW_LINE +
  947 + MARKDOWN_CODE_BLOCK_START +
  948 + "{\n" +
  949 + " \"type\": \"TIME_SERIES\",\n" +
  950 + " \"key\": \"temperature\"\n" +
  951 + "}" +
  952 + MARKDOWN_CODE_BLOCK_END;
  953 +
  954 + protected static final String DEVICE_PROFILE_FILTER_PREDICATE = NEW_LINE + "## Filter Predicate" + NEW_LINE +
  955 + "Filter Predicate defines the logical expression to evaluate. The list of available operations depends on the filter value type, see above. " +
  956 + "Platform supports 4 predicate types: 'STRING', 'NUMERIC', 'BOOLEAN' and 'COMPLEX'. The last one allows to combine multiple operations over one filter key." + NEW_LINE +
  957 + "Simple predicate example to check 'value < 100': " + NEW_LINE +
  958 + MARKDOWN_CODE_BLOCK_START +
  959 + "{\n" +
  960 + " \"operation\": \"LESS\",\n" +
  961 + " \"value\": {\n" +
  962 + " \"userValue\": null,\n" +
  963 + " \"defaultValue\": 100,\n" +
  964 + " \"dynamicValue\": null\n" +
  965 + " },\n" +
  966 + " \"type\": \"NUMERIC\"\n" +
  967 + "}" +
  968 + MARKDOWN_CODE_BLOCK_END + NEW_LINE +
  969 + "Complex predicate example, to check 'value < 10 or value > 20': " + NEW_LINE +
  970 + MARKDOWN_CODE_BLOCK_START +
  971 + "{\n" +
  972 + " \"type\": \"COMPLEX\",\n" +
  973 + " \"operation\": \"OR\",\n" +
  974 + " \"predicates\": [\n" +
  975 + " {\n" +
  976 + " \"operation\": \"LESS\",\n" +
  977 + " \"value\": {\n" +
  978 + " \"userValue\": null,\n" +
  979 + " \"defaultValue\": 10,\n" +
  980 + " \"dynamicValue\": null\n" +
  981 + " },\n" +
  982 + " \"type\": \"NUMERIC\"\n" +
  983 + " },\n" +
  984 + " {\n" +
  985 + " \"operation\": \"GREATER\",\n" +
  986 + " \"value\": {\n" +
  987 + " \"userValue\": null,\n" +
  988 + " \"defaultValue\": 20,\n" +
  989 + " \"dynamicValue\": null\n" +
  990 + " },\n" +
  991 + " \"type\": \"NUMERIC\"\n" +
  992 + " }\n" +
  993 + " ]\n" +
  994 + "}" +
  995 + MARKDOWN_CODE_BLOCK_END + NEW_LINE +
  996 + "More complex predicate example, to check 'value < 10 or (value > 50 && value < 60)': " + NEW_LINE +
  997 + MARKDOWN_CODE_BLOCK_START +
  998 + "{\n" +
  999 + " \"type\": \"COMPLEX\",\n" +
  1000 + " \"operation\": \"OR\",\n" +
  1001 + " \"predicates\": [\n" +
  1002 + " {\n" +
  1003 + " \"operation\": \"LESS\",\n" +
  1004 + " \"value\": {\n" +
  1005 + " \"userValue\": null,\n" +
  1006 + " \"defaultValue\": 10,\n" +
  1007 + " \"dynamicValue\": null\n" +
  1008 + " },\n" +
  1009 + " \"type\": \"NUMERIC\"\n" +
  1010 + " },\n" +
  1011 + " {\n" +
  1012 + " \"type\": \"COMPLEX\",\n" +
  1013 + " \"operation\": \"AND\",\n" +
  1014 + " \"predicates\": [\n" +
  1015 + " {\n" +
  1016 + " \"operation\": \"GREATER\",\n" +
  1017 + " \"value\": {\n" +
  1018 + " \"userValue\": null,\n" +
  1019 + " \"defaultValue\": 50,\n" +
  1020 + " \"dynamicValue\": null\n" +
  1021 + " },\n" +
  1022 + " \"type\": \"NUMERIC\"\n" +
  1023 + " },\n" +
  1024 + " {\n" +
  1025 + " \"operation\": \"LESS\",\n" +
  1026 + " \"value\": {\n" +
  1027 + " \"userValue\": null,\n" +
  1028 + " \"defaultValue\": 60,\n" +
  1029 + " \"dynamicValue\": null\n" +
  1030 + " },\n" +
  1031 + " \"type\": \"NUMERIC\"\n" +
  1032 + " }\n" +
  1033 + " ]\n" +
  1034 + " }\n" +
  1035 + " ]\n" +
  1036 + "}" +
  1037 + MARKDOWN_CODE_BLOCK_END + NEW_LINE +
  1038 + "You may also want to replace hardcoded values (for example, temperature > 20) with the more dynamic " +
  1039 + "expression (for example, temperature > value of the tenant attribute with key 'temperatureThreshold'). " +
  1040 + "It is possible to use 'dynamicValue' to define attribute of the tenant, customer or device. " +
  1041 + "See example below:" + NEW_LINE +
  1042 + MARKDOWN_CODE_BLOCK_START +
  1043 + "{\n" +
  1044 + " \"operation\": \"GREATER\",\n" +
  1045 + " \"value\": {\n" +
  1046 + " \"userValue\": null,\n" +
  1047 + " \"defaultValue\": 0,\n" +
  1048 + " \"dynamicValue\": {\n" +
  1049 + " \"inherit\": false,\n" +
  1050 + " \"sourceType\": \"CURRENT_TENANT\",\n" +
  1051 + " \"sourceAttribute\": \"temperatureThreshold\"\n" +
  1052 + " }\n" +
  1053 + " },\n" +
  1054 + " \"type\": \"NUMERIC\"\n" +
  1055 + "}" +
  1056 + MARKDOWN_CODE_BLOCK_END + NEW_LINE +
  1057 + "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. " +
  1058 + "The 'sourceAttribute' can be inherited from the owner of the specified 'sourceType' if 'inherit' is set to true.";
  1059 +
  1060 + protected static final String KEY_FILTERS_DESCRIPTION = "# Key Filters" + NEW_LINE +
  1061 + "Key filter objects are created under the **'condition'** array. They allow you to define complex logical expressions over entity field, " +
  1062 + "attribute, latest time-series value or constant. The filter is defined using 'key', 'valueType', " +
  1063 + "'value' (refers to the value of the 'CONSTANT' alarm filter key type) and 'predicate' objects. Let's review each object:" + NEW_LINE +
  1064 + ALARM_FILTER_KEY + FILTER_VALUE_TYPE + NEW_LINE + DEVICE_PROFILE_FILTER_PREDICATE + NEW_LINE;
  1065 +
  1066 + protected static final String DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
  1067 + " \"alarms\":[\n" +
  1068 + " ],\n" +
  1069 + " \"configuration\":{\n" +
  1070 + " \"type\":\"DEFAULT\"\n" +
  1071 + " },\n" +
  1072 + " \"provisionConfiguration\":{\n" +
  1073 + " \"type\":\"DISABLED\",\n" +
  1074 + " \"provisionDeviceSecret\":null\n" +
  1075 + " },\n" +
  1076 + " \"transportConfiguration\":{\n" +
  1077 + " \"type\":\"DEFAULT\"\n" +
  1078 + " }\n" +
  1079 + "}" + MARKDOWN_CODE_BLOCK_END;
  1080 +
  1081 + protected static final String CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE = MARKDOWN_CODE_BLOCK_START + "{\n" +
  1082 + " \"alarms\":[\n" +
  1083 + " {\n" +
  1084 + " \"id\":\"2492b935-1226-59e9-8615-17d8978a4f93\",\n" +
  1085 + " \"alarmType\":\"Temperature Alarm\",\n" +
  1086 + " \"clearRule\":{\n" +
  1087 + " \"schedule\":null,\n" +
  1088 + " \"condition\":{\n" +
  1089 + " \"spec\":{\n" +
  1090 + " \"type\":\"SIMPLE\"\n" +
  1091 + " },\n" +
  1092 + " \"condition\":[\n" +
  1093 + " {\n" +
  1094 + " \"key\":{\n" +
  1095 + " \"key\":\"temperature\",\n" +
  1096 + " \"type\":\"TIME_SERIES\"\n" +
  1097 + " },\n" +
  1098 + " \"value\":null,\n" +
  1099 + " \"predicate\":{\n" +
  1100 + " \"type\":\"NUMERIC\",\n" +
  1101 + " \"value\":{\n" +
  1102 + " \"userValue\":null,\n" +
  1103 + " \"defaultValue\":30.0,\n" +
  1104 + " \"dynamicValue\":null\n" +
  1105 + " },\n" +
  1106 + " \"operation\":\"LESS\"\n" +
  1107 + " },\n" +
  1108 + " \"valueType\":\"NUMERIC\"\n" +
  1109 + " }\n" +
  1110 + " ]\n" +
  1111 + " },\n" +
  1112 + " \"dashboardId\":null,\n" +
  1113 + " \"alarmDetails\":null\n" +
  1114 + " },\n" +
  1115 + " \"propagate\":false,\n" +
  1116 + " \"createRules\":{\n" +
  1117 + " \"MAJOR\":{\n" +
  1118 + " \"schedule\":{\n" +
  1119 + " \"type\":\"SPECIFIC_TIME\",\n" +
  1120 + " \"endsOn\":64800000,\n" +
  1121 + " \"startsOn\":43200000,\n" +
  1122 + " \"timezone\":\"Europe/Kiev\",\n" +
  1123 + " \"daysOfWeek\":[\n" +
  1124 + " 1,\n" +
  1125 + " 3,\n" +
  1126 + " 5\n" +
  1127 + " ]\n" +
  1128 + " },\n" +
  1129 + " \"condition\":{\n" +
  1130 + " \"spec\":{\n" +
  1131 + " \"type\":\"DURATION\",\n" +
  1132 + " \"unit\":\"MINUTES\",\n" +
  1133 + " \"predicate\":{\n" +
  1134 + " \"userValue\":null,\n" +
  1135 + " \"defaultValue\":30,\n" +
  1136 + " \"dynamicValue\":null\n" +
  1137 + " }\n" +
  1138 + " },\n" +
  1139 + " \"condition\":[\n" +
  1140 + " {\n" +
  1141 + " \"key\":{\n" +
  1142 + " \"key\":\"temperature\",\n" +
  1143 + " \"type\":\"TIME_SERIES\"\n" +
  1144 + " },\n" +
  1145 + " \"value\":null,\n" +
  1146 + " \"predicate\":{\n" +
  1147 + " \"type\":\"COMPLEX\",\n" +
  1148 + " \"operation\":\"OR\",\n" +
  1149 + " \"predicates\":[\n" +
  1150 + " {\n" +
  1151 + " \"type\":\"NUMERIC\",\n" +
  1152 + " \"value\":{\n" +
  1153 + " \"userValue\":null,\n" +
  1154 + " \"defaultValue\":50.0,\n" +
  1155 + " \"dynamicValue\":null\n" +
  1156 + " },\n" +
  1157 + " \"operation\":\"LESS_OR_EQUAL\"\n" +
  1158 + " },\n" +
  1159 + " {\n" +
  1160 + " \"type\":\"NUMERIC\",\n" +
  1161 + " \"value\":{\n" +
  1162 + " \"userValue\":null,\n" +
  1163 + " \"defaultValue\":30.0,\n" +
  1164 + " \"dynamicValue\":null\n" +
  1165 + " },\n" +
  1166 + " \"operation\":\"GREATER\"\n" +
  1167 + " }\n" +
  1168 + " ]\n" +
  1169 + " },\n" +
  1170 + " \"valueType\":\"NUMERIC\"\n" +
  1171 + " }\n" +
  1172 + " ]\n" +
  1173 + " },\n" +
  1174 + " \"dashboardId\":null,\n" +
  1175 + " \"alarmDetails\":null\n" +
  1176 + " },\n" +
  1177 + " \"WARNING\":{\n" +
  1178 + " \"schedule\":{\n" +
  1179 + " \"type\":\"CUSTOM\",\n" +
  1180 + " \"items\":[\n" +
  1181 + " {\n" +
  1182 + " \"endsOn\":0,\n" +
  1183 + " \"enabled\":false,\n" +
  1184 + " \"startsOn\":0,\n" +
  1185 + " \"dayOfWeek\":1\n" +
  1186 + " },\n" +
  1187 + " {\n" +
  1188 + " \"endsOn\":64800000,\n" +
  1189 + " \"enabled\":true,\n" +
  1190 + " \"startsOn\":43200000,\n" +
  1191 + " \"dayOfWeek\":2\n" +
  1192 + " },\n" +
  1193 + " {\n" +
  1194 + " \"endsOn\":0,\n" +
  1195 + " \"enabled\":false,\n" +
  1196 + " \"startsOn\":0,\n" +
  1197 + " \"dayOfWeek\":3\n" +
  1198 + " },\n" +
  1199 + " {\n" +
  1200 + " \"endsOn\":57600000,\n" +
  1201 + " \"enabled\":true,\n" +
  1202 + " \"startsOn\":36000000,\n" +
  1203 + " \"dayOfWeek\":4\n" +
  1204 + " },\n" +
  1205 + " {\n" +
  1206 + " \"endsOn\":0,\n" +
  1207 + " \"enabled\":false,\n" +
  1208 + " \"startsOn\":0,\n" +
  1209 + " \"dayOfWeek\":5\n" +
  1210 + " },\n" +
  1211 + " {\n" +
  1212 + " \"endsOn\":0,\n" +
  1213 + " \"enabled\":false,\n" +
  1214 + " \"startsOn\":0,\n" +
  1215 + " \"dayOfWeek\":6\n" +
  1216 + " },\n" +
  1217 + " {\n" +
  1218 + " \"endsOn\":0,\n" +
  1219 + " \"enabled\":false,\n" +
  1220 + " \"startsOn\":0,\n" +
  1221 + " \"dayOfWeek\":7\n" +
  1222 + " }\n" +
  1223 + " ],\n" +
  1224 + " \"timezone\":\"Europe/Kiev\"\n" +
  1225 + " },\n" +
  1226 + " \"condition\":{\n" +
  1227 + " \"spec\":{\n" +
  1228 + " \"type\":\"REPEATING\",\n" +
  1229 + " \"predicate\":{\n" +
  1230 + " \"userValue\":null,\n" +
  1231 + " \"defaultValue\":5,\n" +
  1232 + " \"dynamicValue\":null\n" +
  1233 + " }\n" +
  1234 + " },\n" +
  1235 + " \"condition\":[\n" +
  1236 + " {\n" +
  1237 + " \"key\":{\n" +
  1238 + " \"key\":\"tempConstant\",\n" +
  1239 + " \"type\":\"CONSTANT\"\n" +
  1240 + " },\n" +
  1241 + " \"value\":30,\n" +
  1242 + " \"predicate\":{\n" +
  1243 + " \"type\":\"NUMERIC\",\n" +
  1244 + " \"value\":{\n" +
  1245 + " \"userValue\":null,\n" +
  1246 + " \"defaultValue\":0.0,\n" +
  1247 + " \"dynamicValue\":{\n" +
  1248 + " \"inherit\":false,\n" +
  1249 + " \"sourceType\":\"CURRENT_DEVICE\",\n" +
  1250 + " \"sourceAttribute\":\"tempThreshold\"\n" +
  1251 + " }\n" +
  1252 + " },\n" +
  1253 + " \"operation\":\"EQUAL\"\n" +
  1254 + " },\n" +
  1255 + " \"valueType\":\"NUMERIC\"\n" +
  1256 + " }\n" +
  1257 + " ]\n" +
  1258 + " },\n" +
  1259 + " \"dashboardId\":null,\n" +
  1260 + " \"alarmDetails\":null\n" +
  1261 + " },\n" +
  1262 + " \"CRITICAL\":{\n" +
  1263 + " \"schedule\":null,\n" +
  1264 + " \"condition\":{\n" +
  1265 + " \"spec\":{\n" +
  1266 + " \"type\":\"SIMPLE\"\n" +
  1267 + " },\n" +
  1268 + " \"condition\":[\n" +
  1269 + " {\n" +
  1270 + " \"key\":{\n" +
  1271 + " \"key\":\"temperature\",\n" +
  1272 + " \"type\":\"TIME_SERIES\"\n" +
  1273 + " },\n" +
  1274 + " \"value\":null,\n" +
  1275 + " \"predicate\":{\n" +
  1276 + " \"type\":\"NUMERIC\",\n" +
  1277 + " \"value\":{\n" +
  1278 + " \"userValue\":null,\n" +
  1279 + " \"defaultValue\":50.0,\n" +
  1280 + " \"dynamicValue\":null\n" +
  1281 + " },\n" +
  1282 + " \"operation\":\"GREATER\"\n" +
  1283 + " },\n" +
  1284 + " \"valueType\":\"NUMERIC\"\n" +
  1285 + " }\n" +
  1286 + " ]\n" +
  1287 + " },\n" +
  1288 + " \"dashboardId\":null,\n" +
  1289 + " \"alarmDetails\":null\n" +
  1290 + " }\n" +
  1291 + " },\n" +
  1292 + " \"propagateRelationTypes\":null\n" +
  1293 + " }\n" +
  1294 + " ],\n" +
  1295 + " \"configuration\":{\n" +
  1296 + " \"type\":\"DEFAULT\"\n" +
  1297 + " },\n" +
  1298 + " \"provisionConfiguration\":{\n" +
  1299 + " \"type\":\"ALLOW_CREATE_NEW_DEVICES\",\n" +
  1300 + " \"provisionDeviceSecret\":\"vaxb9hzqdbz3oqukvomg\"\n" +
  1301 + " },\n" +
  1302 + " \"transportConfiguration\":{\n" +
  1303 + " \"type\":\"MQTT\",\n" +
  1304 + " \"deviceTelemetryTopic\":\"v1/devices/me/telemetry\",\n" +
  1305 + " \"deviceAttributesTopic\":\"v1/devices/me/attributes\",\n" +
  1306 + " \"transportPayloadTypeConfiguration\":{\n" +
  1307 + " \"transportPayloadType\":\"PROTOBUF\",\n" +
  1308 + " \"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" +
  1309 + " \"deviceAttributesProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage attributes;\\n\\nmessage SensorConfiguration {\\n optional string firmwareVersion = 1;\\n optional string serialNumber = 2;\\n}\",\n" +
  1310 + " \"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" +
  1311 + " \"deviceRpcResponseProtoSchema\":\"syntax =\\\"proto3\\\";\\npackage rpc;\\n\\nmessage RpcResponseMsg {\\n optional string payload = 1;\\n}\"\n" +
  1312 + " }\n" +
  1313 + " }\n" +
  1314 + "}" + MARKDOWN_CODE_BLOCK_END;
  1315 + protected static final String DEVICE_PROFILE_DATA_DEFINITION = NEW_LINE + "# Device profile data definition" + NEW_LINE +
  1316 + "Device profile data object contains alarm rules configuration, device provision strategy and transport type configuration for device connectivity. Let's review some examples. " +
  1317 + "First one is the default device profile data configuration and second one - the custom one. " +
  1318 + NEW_LINE + DEFAULT_DEVICE_PROFILE_DATA_EXAMPLE + NEW_LINE + CUSTOM_DEVICE_PROFILE_DATA_EXAMPLE +
  1319 + NEW_LINE + "Let's review some specific objects examples related to the device profile configuration:";
  1320 +
  1321 + protected static final String ALARM_SCHEDULE = NEW_LINE + "# Alarm Schedule" + NEW_LINE +
  1322 + "Alarm Schedule JSON object represents the time interval during which the alarm rule is active. Note, " +
  1323 + NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_ALWAYS_EXAMPLE + NEW_LINE + "means alarm rule is active all the time. " +
  1324 + "**'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). " +
  1325 + "**'enabled'** flag specifies if item in a custom rule is active for specific day of the week:" + NEW_LINE +
  1326 + "## Specific Time Schedule" + NEW_LINE +
  1327 + DEVICE_PROFILE_ALARM_SCHEDULE_SPECIFIC_TIME_EXAMPLE + NEW_LINE +
  1328 + "## Custom Schedule" +
  1329 + NEW_LINE + DEVICE_PROFILE_ALARM_SCHEDULE_CUSTOM_EXAMPLE + NEW_LINE;
  1330 +
  1331 + protected static final String ALARM_CONDITION_TYPE = "# Alarm condition type (**'spec'**)" + NEW_LINE +
  1332 + "Alarm condition type can be either simple, duration, or repeating. For example, 5 times in a row or during 5 minutes." + NEW_LINE +
  1333 + "Note, **'userValue'** field is not used and reserved for future usage, **'dynamicValue'** is used for condition appliance by using the value of the **'sourceAttribute'** " +
  1334 + "or else **'defaultValue'** is used (if **'sourceAttribute'** is absent).\n" +
  1335 + "\n**'sourceType'** of the **'sourceAttribute'** can be: \n" +
  1336 + " * 'CURRENT_DEVICE';\n" +
  1337 + " * 'CURRENT_CUSTOMER';\n" +
  1338 + " * 'CURRENT_TENANT'." + NEW_LINE +
  1339 + "**'sourceAttribute'** can be inherited from the owner if **'inherit'** is set to true (for CURRENT_DEVICE and CURRENT_CUSTOMER)." + NEW_LINE +
  1340 + "## Repeating alarm condition" + NEW_LINE +
  1341 + DEVICE_PROFILE_ALARM_CONDITION_REPEATING_EXAMPLE + NEW_LINE +
  1342 + "## Duration alarm condition" + NEW_LINE +
  1343 + DEVICE_PROFILE_ALARM_CONDITION_DURATION_EXAMPLE + NEW_LINE +
  1344 + "**'unit'** can be: \n" +
  1345 + " * 'SECONDS';\n" +
  1346 + " * 'MINUTES';\n" +
  1347 + " * 'HOURS';\n" +
  1348 + " * 'DAYS'." + NEW_LINE;
  1349 +
  1350 + protected static final String PROVISION_CONFIGURATION = "# Provision Configuration" + NEW_LINE +
  1351 + "There are 3 types of device provision configuration for the device profile: \n" +
  1352 + " * 'DISABLED';\n" +
  1353 + " * 'ALLOW_CREATE_NEW_DEVICES';\n" +
  1354 + " * 'CHECK_PRE_PROVISIONED_DEVICES'." + NEW_LINE +
  1355 + "Please refer to the [docs](https://thingsboard.io/docs/user-guide/device-provisioning/) for more details." + NEW_LINE;
  1356 +
  1357 + protected static final String DEVICE_PROFILE_DATA = DEVICE_PROFILE_DATA_DEFINITION + ALARM_SCHEDULE + ALARM_CONDITION_TYPE +
  1358 + KEY_FILTERS_DESCRIPTION + PROVISION_CONFIGURATION + TRANSPORT_CONFIGURATION;
  1359 +
  1360 + protected static final String DEVICE_PROFILE_ID = "deviceProfileId";
  1361 +
  1362 +}
... ...
... ... @@ -18,6 +18,8 @@ package org.thingsboard.server.controller;
18 18 import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 20 import com.fasterxml.jackson.databind.node.ObjectNode;
  21 +import io.swagger.annotations.ApiOperation;
  22 +import io.swagger.annotations.ApiParam;
21 23 import org.springframework.http.HttpStatus;
22 24 import org.springframework.security.access.prepost.PreAuthorize;
23 25 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -45,23 +47,43 @@ import org.thingsboard.server.service.security.permission.Resource;
45 47
46 48 import java.util.List;
47 49
  50 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
  51 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
  52 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES;
  53 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_TEXT_SEARCH_DESCRIPTION;
  54 +import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
  55 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  56 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  57 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  58 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  59 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  60 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  61 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  62 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  63 +
48 64 @RestController
49 65 @TbCoreComponent
50 66 @RequestMapping("/api")
51 67 public class CustomerController extends BaseController {
52 68
53   - public static final String CUSTOMER_ID = "customerId";
54 69 public static final String IS_PUBLIC = "isPublic";
  70 + public static final String CUSTOMER_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the customer is owned by the same tenant. " +
  71 + "If the user has the authority of 'Customer User', the server checks that the user belongs to the customer.";
55 72
  73 + @ApiOperation(value = "Get Customer (getCustomerById)",
  74 + notes = "Get the Customer object based on the provided Customer Id. "
  75 + + CUSTOMER_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
56 76 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
57 77 @RequestMapping(value = "/customer/{customerId}", method = RequestMethod.GET)
58 78 @ResponseBody
59   - public Customer getCustomerById(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
  79 + public Customer getCustomerById(
  80 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  81 + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
60 82 checkParameter(CUSTOMER_ID, strCustomerId);
61 83 try {
62 84 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
63 85 Customer customer = checkCustomerId(customerId, Operation.READ);
64   - if(!customer.getAdditionalInfo().isNull()) {
  86 + if (!customer.getAdditionalInfo().isNull()) {
65 87 processDashboardIdFromAdditionalInfo((ObjectNode) customer.getAdditionalInfo(), HOME_DASHBOARD);
66 88 }
67 89 return customer;
... ... @@ -70,10 +92,16 @@ public class CustomerController extends BaseController {
70 92 }
71 93 }
72 94
  95 +
  96 + @ApiOperation(value = "Get short Customer info (getShortCustomerInfoById)",
  97 + notes = "Get the short customer object that contains only the title and 'isPublic' flag. "
  98 + + CUSTOMER_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
73 99 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
74 100 @RequestMapping(value = "/customer/{customerId}/shortInfo", method = RequestMethod.GET)
75 101 @ResponseBody
76   - public JsonNode getShortCustomerInfoById(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
  102 + public JsonNode getShortCustomerInfoById(
  103 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  104 + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
77 105 checkParameter(CUSTOMER_ID, strCustomerId);
78 106 try {
79 107 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
... ... @@ -88,10 +116,15 @@ public class CustomerController extends BaseController {
88 116 }
89 117 }
90 118
  119 + @ApiOperation(value = "Get Customer Title (getCustomerTitleById)",
  120 + notes = "Get the title of the customer. "
  121 + + CUSTOMER_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
91 122 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
92 123 @RequestMapping(value = "/customer/{customerId}/title", method = RequestMethod.GET, produces = "application/text")
93 124 @ResponseBody
94   - public String getCustomerTitleById(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
  125 + public String getCustomerTitleById(
  126 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  127 + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
95 128 checkParameter(CUSTOMER_ID, strCustomerId);
96 129 try {
97 130 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
... ... @@ -102,10 +135,15 @@ public class CustomerController extends BaseController {
102 135 }
103 136 }
104 137
  138 + @ApiOperation(value = "Create or update Customer (saveCustomer)",
  139 + 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) " +
  140 + "The newly created Customer Id will be present in the response. " +
  141 + "Specify existing Customer Id to update the Customer. " +
  142 + "Referencing non-existing Customer Id will cause 'Not Found' error." + TENANT_AUTHORITY_PARAGRAPH)
105 143 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
106 144 @RequestMapping(value = "/customer", method = RequestMethod.POST)
107 145 @ResponseBody
108   - public Customer saveCustomer(@RequestBody Customer customer) throws ThingsboardException {
  146 + public Customer saveCustomer(@ApiParam(value = "A JSON value representing the customer.") @RequestBody Customer customer) throws ThingsboardException {
109 147 try {
110 148 customer.setTenantId(getCurrentUser().getTenantId());
111 149
... ... @@ -131,10 +169,15 @@ public class CustomerController extends BaseController {
131 169 }
132 170 }
133 171
  172 + @ApiOperation(value = "Delete Customer (deleteCustomer)",
  173 + notes = "Deletes the Customer and all customer Users. " +
  174 + "All assigned Dashboards, Assets, Devices, etc. will be unassigned but not deleted. " +
  175 + "Referencing non-existing Customer Id will cause an error." + TENANT_AUTHORITY_PARAGRAPH)
134 176 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
135 177 @RequestMapping(value = "/customer/{customerId}", method = RequestMethod.DELETE)
136 178 @ResponseStatus(value = HttpStatus.OK)
137   - public void deleteCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
  179 + public void deleteCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  180 + @PathVariable(CUSTOMER_ID) String strCustomerId) throws ThingsboardException {
138 181 checkParameter(CUSTOMER_ID, strCustomerId);
139 182 try {
140 183 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
... ... @@ -161,14 +204,23 @@ public class CustomerController extends BaseController {
161 204 }
162 205 }
163 206
  207 + @ApiOperation(value = "Get Tenant Customers (getCustomers)",
  208 + notes = "Returns a page of customers owned by tenant. " +
  209 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
164 210 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
165 211 @RequestMapping(value = "/customers", params = {"pageSize", "page"}, method = RequestMethod.GET)
166 212 @ResponseBody
167   - public PageData<Customer> getCustomers(@RequestParam int pageSize,
168   - @RequestParam int page,
169   - @RequestParam(required = false) String textSearch,
170   - @RequestParam(required = false) String sortProperty,
171   - @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  213 + public PageData<Customer> getCustomers(
  214 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
  215 + @RequestParam int pageSize,
  216 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
  217 + @RequestParam int page,
  218 + @ApiParam(value = CUSTOMER_TEXT_SEARCH_DESCRIPTION)
  219 + @RequestParam(required = false) String textSearch,
  220 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES)
  221 + @RequestParam(required = false) String sortProperty,
  222 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
  223 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
172 224 try {
173 225 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
174 226 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -178,10 +230,13 @@ public class CustomerController extends BaseController {
178 230 }
179 231 }
180 232
  233 + @ApiOperation(value = "Get Tenant Customer by Customer title (getTenantCustomer)",
  234 + notes = "Get the Customer using Customer Title. " + TENANT_AUTHORITY_PARAGRAPH)
181 235 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
182 236 @RequestMapping(value = "/tenant/customers", params = {"customerTitle"}, method = RequestMethod.GET)
183 237 @ResponseBody
184 238 public Customer getTenantCustomer(
  239 + @ApiParam(value = "A string value representing the Customer title.")
185 240 @RequestParam String customerTitle) throws ThingsboardException {
186 241 try {
187 242 TenantId tenantId = getCurrentUser().getTenantId();
... ...
... ... @@ -17,8 +17,11 @@ package org.thingsboard.server.controller;
17 17
18 18 import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.fasterxml.jackson.databind.node.ObjectNode;
  20 +import io.swagger.annotations.ApiOperation;
  21 +import io.swagger.annotations.ApiParam;
20 22 import org.springframework.beans.factory.annotation.Value;
21 23 import org.springframework.http.HttpStatus;
  24 +import org.springframework.http.MediaType;
22 25 import org.springframework.security.access.prepost.PreAuthorize;
23 26 import org.springframework.web.bind.annotation.PathVariable;
24 27 import org.springframework.web.bind.annotation.RequestBody;
... ... @@ -28,6 +31,7 @@ import org.springframework.web.bind.annotation.RequestParam;
28 31 import org.springframework.web.bind.annotation.ResponseBody;
29 32 import org.springframework.web.bind.annotation.ResponseStatus;
30 33 import org.springframework.web.bind.annotation.RestController;
  34 +import org.thingsboard.common.util.JacksonUtil;
31 35 import org.thingsboard.server.common.data.Customer;
32 36 import org.thingsboard.server.common.data.Dashboard;
33 37 import org.thingsboard.server.common.data.DashboardInfo;
... ... @@ -47,7 +51,6 @@ import org.thingsboard.server.common.data.id.EdgeId;
47 51 import org.thingsboard.server.common.data.id.TenantId;
48 52 import org.thingsboard.server.common.data.page.PageData;
49 53 import org.thingsboard.server.common.data.page.PageLink;
50   -import org.thingsboard.common.util.JacksonUtil;
51 54 import org.thingsboard.server.common.data.page.TimePageLink;
52 55 import org.thingsboard.server.queue.util.TbCoreComponent;
53 56 import org.thingsboard.server.service.security.model.SecurityUser;
... ... @@ -59,6 +62,27 @@ import java.util.List;
59 62 import java.util.Set;
60 63 import java.util.stream.Collectors;
61 64
  65 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
  66 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
  67 +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_ID_PARAM_DESCRIPTION;
  68 +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES;
  69 +import static org.thingsboard.server.controller.ControllerConstants.DASHBOARD_TEXT_SEARCH_DESCRIPTION;
  70 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
  71 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
  72 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
  73 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
  74 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  75 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  76 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  77 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  78 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  79 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  80 +import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
  81 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  82 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID;
  83 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
  84 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  85 +
62 86 @RestController
63 87 @TbCoreComponent
64 88 @RequestMapping("/api")
... ... @@ -68,11 +92,16 @@ public class DashboardController extends BaseController {
68 92
69 93 private static final String HOME_DASHBOARD_ID = "homeDashboardId";
70 94 private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
  95 + public static final String DASHBOARD_INFO_DEFINITION = "The Dashboard Info object contains lightweight information about the dashboard (e.g. title, image, assigned customers) but does not contain the heavyweight configuration JSON.";
  96 + 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).";
  97 + public static final String HIDDEN_FOR_MOBILE = "Exclude dashboards that are hidden for mobile";
71 98
72 99 @Value("${dashboard.max_datapoints_limit}")
73 100 private long maxDatapointsLimit;
74 101
75   -
  102 + @ApiOperation(value = "Get server time (getServerTime)",
  103 + notes = "Get the server time (milliseconds since January 1, 1970 UTC). " +
  104 + "Used to adjust view of the dashboards according to the difference between browser and server time.")
76 105 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
77 106 @RequestMapping(value = "/dashboard/serverTime", method = RequestMethod.GET)
78 107 @ResponseBody
... ... @@ -80,6 +109,11 @@ public class DashboardController extends BaseController {
80 109 return System.currentTimeMillis();
81 110 }
82 111
  112 + @ApiOperation(value = "Get max data points limit (getMaxDatapointsLimit)",
  113 + notes = "Get the maximum number of data points that dashboard may request from the server per in a single subscription command. " +
  114 + "This value impacts the time window behavior. It impacts 'Max values' parameter in case user selects 'None' as 'Data aggregation function'. " +
  115 + "It also impacts the 'Grouping interval' in case of any other 'Data aggregation function' is selected. " +
  116 + "The actual value of the limit is configurable in the system configuration file.")
83 117 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
84 118 @RequestMapping(value = "/dashboard/maxDatapointsLimit", method = RequestMethod.GET)
85 119 @ResponseBody
... ... @@ -87,10 +121,16 @@ public class DashboardController extends BaseController {
87 121 return maxDatapointsLimit;
88 122 }
89 123
  124 + @ApiOperation(value = "Get Dashboard Info (getDashboardInfoById)",
  125 + notes = "Get the information about the dashboard based on 'dashboardId' parameter. " + DASHBOARD_INFO_DEFINITION,
  126 + produces = MediaType.APPLICATION_JSON_VALUE
  127 + )
90 128 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
91 129 @RequestMapping(value = "/dashboard/info/{dashboardId}", method = RequestMethod.GET)
92 130 @ResponseBody
93   - public DashboardInfo getDashboardInfoById(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  131 + public DashboardInfo getDashboardInfoById(
  132 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  133 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
94 134 checkParameter(DASHBOARD_ID, strDashboardId);
95 135 try {
96 136 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -100,10 +140,16 @@ public class DashboardController extends BaseController {
100 140 }
101 141 }
102 142
  143 + @ApiOperation(value = "Get Dashboard (getDashboardById)",
  144 + notes = "Get the dashboard based on 'dashboardId' parameter. " + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  145 + produces = MediaType.APPLICATION_JSON_VALUE
  146 + )
103 147 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
104 148 @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.GET)
105 149 @ResponseBody
106   - public Dashboard getDashboardById(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  150 + public Dashboard getDashboardById(
  151 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  152 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
107 153 checkParameter(DASHBOARD_ID, strDashboardId);
108 154 try {
109 155 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -113,10 +159,20 @@ public class DashboardController extends BaseController {
113 159 }
114 160 }
115 161
  162 + @ApiOperation(value = "Create Or Update Dashboard (saveDashboard)",
  163 + 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)." +
  164 + "The newly created Dashboard id will be present in the response. " +
  165 + "Specify existing Dashboard id to update the dashboard. " +
  166 + "Referencing non-existing dashboard Id will cause 'Not Found' error. " +
  167 + TENANT_AUTHORITY_PARAGRAPH,
  168 + produces = MediaType.APPLICATION_JSON_VALUE,
  169 + consumes = MediaType.APPLICATION_JSON_VALUE)
116 170 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
117 171 @RequestMapping(value = "/dashboard", method = RequestMethod.POST)
118 172 @ResponseBody
119   - public Dashboard saveDashboard(@RequestBody Dashboard dashboard) throws ThingsboardException {
  173 + public Dashboard saveDashboard(
  174 + @ApiParam(value = "A JSON value representing the dashboard.")
  175 + @RequestBody Dashboard dashboard) throws ThingsboardException {
120 176 try {
121 177 dashboard.setTenantId(getCurrentUser().getTenantId());
122 178
... ... @@ -141,10 +197,14 @@ public class DashboardController extends BaseController {
141 197 }
142 198 }
143 199
  200 + @ApiOperation(value = "Delete the Dashboard (deleteDashboard)",
  201 + notes = "Delete the Dashboard." + TENANT_AUTHORITY_PARAGRAPH)
144 202 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
145 203 @RequestMapping(value = "/dashboard/{dashboardId}", method = RequestMethod.DELETE)
146 204 @ResponseStatus(value = HttpStatus.OK)
147   - public void deleteDashboard(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  205 + public void deleteDashboard(
  206 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  207 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
148 208 checkParameter(DASHBOARD_ID, strDashboardId);
149 209 try {
150 210 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -170,12 +230,19 @@ public class DashboardController extends BaseController {
170 230 }
171 231 }
172 232
  233 + @ApiOperation(value = "Assign the Dashboard (assignDashboardToCustomer)",
  234 + notes = "Assign the Dashboard to specified Customer or do nothing if the Dashboard is already assigned to that Customer. " +
  235 + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH,
  236 + produces = MediaType.APPLICATION_JSON_VALUE)
173 237 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
174 238 @RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.POST)
175 239 @ResponseBody
176   - public Dashboard assignDashboardToCustomer(@PathVariable("customerId") String strCustomerId,
177   - @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
178   - checkParameter("customerId", strCustomerId);
  240 + public Dashboard assignDashboardToCustomer(
  241 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  242 + @PathVariable(CUSTOMER_ID) String strCustomerId,
  243 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  244 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  245 + checkParameter(CUSTOMER_ID, strCustomerId);
179 246 checkParameter(DASHBOARD_ID, strDashboardId);
180 247 try {
181 248 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
... ... @@ -203,11 +270,18 @@ public class DashboardController extends BaseController {
203 270 }
204 271 }
205 272
  273 + @ApiOperation(value = "Unassign the Dashboard (unassignDashboardFromCustomer)",
  274 + notes = "Unassign the Dashboard from specified Customer or do nothing if the Dashboard is already assigned to that Customer. " +
  275 + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH,
  276 + produces = MediaType.APPLICATION_JSON_VALUE)
206 277 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
207 278 @RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.DELETE)
208 279 @ResponseBody
209   - public Dashboard unassignDashboardFromCustomer(@PathVariable("customerId") String strCustomerId,
210   - @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  280 + public Dashboard unassignDashboardFromCustomer(
  281 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  282 + @PathVariable(CUSTOMER_ID) String strCustomerId,
  283 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  284 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
211 285 checkParameter("customerId", strCustomerId);
212 286 checkParameter(DASHBOARD_ID, strDashboardId);
213 287 try {
... ... @@ -235,11 +309,20 @@ public class DashboardController extends BaseController {
235 309 }
236 310 }
237 311
  312 + @ApiOperation(value = "Update the Dashboard Customers (updateDashboardCustomers)",
  313 + notes = "Updates the list of Customers that this Dashboard is assigned to. Removes previous assignments to customers that are not in the provided list. " +
  314 + "Returns the Dashboard object. " + TENANT_AUTHORITY_PARAGRAPH,
  315 + produces = MediaType.APPLICATION_JSON_VALUE,
  316 + consumes = MediaType.APPLICATION_JSON_VALUE)
  317 +
238 318 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
239 319 @RequestMapping(value = "/dashboard/{dashboardId}/customers", method = RequestMethod.POST)
240 320 @ResponseBody
241   - public Dashboard updateDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId,
242   - @RequestBody(required = false) String[] strCustomerIds) throws ThingsboardException {
  321 + public Dashboard updateDashboardCustomers(
  322 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  323 + @PathVariable(DASHBOARD_ID) String strDashboardId,
  324 + @ApiParam(value = "JSON array with the list of customer ids, or empty to remove all customers")
  325 + @RequestBody(required = false) String[] strCustomerIds) throws ThingsboardException {
243 326 checkParameter(DASHBOARD_ID, strDashboardId);
244 327 try {
245 328 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -301,11 +384,19 @@ public class DashboardController extends BaseController {
301 384 }
302 385 }
303 386
  387 + @ApiOperation(value = "Adds the Dashboard Customers (addDashboardCustomers)",
  388 + 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. " +
  389 + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH,
  390 + produces = MediaType.APPLICATION_JSON_VALUE,
  391 + consumes = MediaType.APPLICATION_JSON_VALUE)
304 392 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
305 393 @RequestMapping(value = "/dashboard/{dashboardId}/customers/add", method = RequestMethod.POST)
306 394 @ResponseBody
307   - public Dashboard addDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId,
308   - @RequestBody String[] strCustomerIds) throws ThingsboardException {
  395 + public Dashboard addDashboardCustomers(
  396 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  397 + @PathVariable(DASHBOARD_ID) String strDashboardId,
  398 + @ApiParam(value = "JSON array with the list of customer ids")
  399 + @RequestBody String[] strCustomerIds) throws ThingsboardException {
309 400 checkParameter(DASHBOARD_ID, strDashboardId);
310 401 try {
311 402 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -345,11 +436,19 @@ public class DashboardController extends BaseController {
345 436 }
346 437 }
347 438
  439 + @ApiOperation(value = "Remove the Dashboard Customers (removeDashboardCustomers)",
  440 + 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. " +
  441 + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH,
  442 + produces = MediaType.APPLICATION_JSON_VALUE,
  443 + consumes = MediaType.APPLICATION_JSON_VALUE)
348 444 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
349 445 @RequestMapping(value = "/dashboard/{dashboardId}/customers/remove", method = RequestMethod.POST)
350 446 @ResponseBody
351   - public Dashboard removeDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId,
352   - @RequestBody String[] strCustomerIds) throws ThingsboardException {
  447 + public Dashboard removeDashboardCustomers(
  448 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  449 + @PathVariable(DASHBOARD_ID) String strDashboardId,
  450 + @ApiParam(value = "JSON array with the list of customer ids")
  451 + @RequestBody String[] strCustomerIds) throws ThingsboardException {
353 452 checkParameter(DASHBOARD_ID, strDashboardId);
354 453 try {
355 454 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -389,10 +488,20 @@ public class DashboardController extends BaseController {
389 488 }
390 489 }
391 490
  491 + @ApiOperation(value = "Assign the Dashboard to Public Customer (assignDashboardToPublicCustomer)",
  492 + notes = "Assigns the dashboard to a special, auto-generated 'Public' Customer. Once assigned, unauthenticated users may browse the dashboard. " +
  493 + "This method is useful if you like to embed the dashboard on public web pages to be available for users that are not logged in. " +
  494 + "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." +
  495 + "Use [assign Asset to Public Customer](#!/asset-controller/assignAssetToPublicCustomerUsingPOST) and " +
  496 + "[assign Device to Public Customer](#!/device-controller/assignDeviceToPublicCustomerUsingPOST) for this purpose. " +
  497 + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH,
  498 + produces = MediaType.APPLICATION_JSON_VALUE)
392 499 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
393 500 @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST)
394 501 @ResponseBody
395   - public Dashboard assignDashboardToPublicCustomer(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  502 + public Dashboard assignDashboardToPublicCustomer(
  503 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  504 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
396 505 checkParameter(DASHBOARD_ID, strDashboardId);
397 506 try {
398 507 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -415,10 +524,16 @@ public class DashboardController extends BaseController {
415 524 }
416 525 }
417 526
  527 + @ApiOperation(value = "Unassign the Dashboard from Public Customer (unassignDashboardFromPublicCustomer)",
  528 + notes = "Unassigns the dashboard from a special, auto-generated 'Public' Customer. Once unassigned, unauthenticated users may no longer browse the dashboard. " +
  529 + "Returns the Dashboard object." + TENANT_AUTHORITY_PARAGRAPH,
  530 + produces = MediaType.APPLICATION_JSON_VALUE)
418 531 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
419 532 @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.DELETE)
420 533 @ResponseBody
421   - public Dashboard unassignDashboardFromPublicCustomer(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
  534 + public Dashboard unassignDashboardFromPublicCustomer(
  535 + @ApiParam(value = DASHBOARD_ID_PARAM_DESCRIPTION)
  536 + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException {
422 537 checkParameter(DASHBOARD_ID, strDashboardId);
423 538 try {
424 539 DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
... ... @@ -442,15 +557,25 @@ public class DashboardController extends BaseController {
442 557 }
443 558 }
444 559
  560 + @ApiOperation(value = "Get Tenant Dashboards by System Administrator (getTenantDashboards)",
  561 + notes = "Returns a page of dashboard info objects owned by tenant. " + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS +
  562 + SYSTEM_AUTHORITY_PARAGRAPH,
  563 + produces = MediaType.APPLICATION_JSON_VALUE)
445 564 @PreAuthorize("hasAuthority('SYS_ADMIN')")
446 565 @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
447 566 @ResponseBody
448 567 public PageData<DashboardInfo> getTenantDashboards(
449   - @PathVariable("tenantId") String strTenantId,
  568 + @ApiParam(value = TENANT_ID_PARAM_DESCRIPTION, required = true)
  569 + @PathVariable(TENANT_ID) String strTenantId,
  570 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
450 571 @RequestParam int pageSize,
  572 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
451 573 @RequestParam int page,
  574 + @ApiParam(value = DASHBOARD_TEXT_SEARCH_DESCRIPTION)
452 575 @RequestParam(required = false) String textSearch,
  576 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES)
453 577 @RequestParam(required = false) String sortProperty,
  578 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
454 579 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
455 580 try {
456 581 TenantId tenantId = new TenantId(toUUID(strTenantId));
... ... @@ -462,20 +587,30 @@ public class DashboardController extends BaseController {
462 587 }
463 588 }
464 589
  590 + @ApiOperation(value = "Get Tenant Dashboards (getTenantDashboards)",
  591 + notes = "Returns a page of dashboard info objects owned by the tenant of a current user. "
  592 + + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
  593 + produces = MediaType.APPLICATION_JSON_VALUE)
465 594 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
466 595 @RequestMapping(value = "/tenant/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
467 596 @ResponseBody
468 597 public PageData<DashboardInfo> getTenantDashboards(
  598 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
469 599 @RequestParam int pageSize,
  600 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
470 601 @RequestParam int page,
  602 + @ApiParam(value = HIDDEN_FOR_MOBILE)
471 603 @RequestParam(required = false) Boolean mobile,
  604 + @ApiParam(value = DASHBOARD_TEXT_SEARCH_DESCRIPTION)
472 605 @RequestParam(required = false) String textSearch,
  606 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES)
473 607 @RequestParam(required = false) String sortProperty,
  608 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
474 609 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
475 610 try {
476 611 TenantId tenantId = getCurrentUser().getTenantId();
477 612 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
478   - if (mobile != null && mobile.booleanValue()) {
  613 + if (mobile != null && mobile) {
479 614 return checkNotNull(dashboardService.findMobileDashboardsByTenantId(tenantId, pageLink));
480 615 } else {
481 616 return checkNotNull(dashboardService.findDashboardsByTenantId(tenantId, pageLink));
... ... @@ -485,24 +620,35 @@ public class DashboardController extends BaseController {
485 620 }
486 621 }
487 622
  623 + @ApiOperation(value = "Get Customer Dashboards (getCustomerDashboards)",
  624 + notes = "Returns a page of dashboard info objects owned by the specified customer. "
  625 + + DASHBOARD_INFO_DEFINITION + " " + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  626 + produces = MediaType.APPLICATION_JSON_VALUE)
488 627 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
489 628 @RequestMapping(value = "/customer/{customerId}/dashboards", params = {"pageSize", "page"}, method = RequestMethod.GET)
490 629 @ResponseBody
491 630 public PageData<DashboardInfo> getCustomerDashboards(
492   - @PathVariable("customerId") String strCustomerId,
  631 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
  632 + @PathVariable(CUSTOMER_ID) String strCustomerId,
  633 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
493 634 @RequestParam int pageSize,
  635 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
494 636 @RequestParam int page,
  637 + @ApiParam(value = HIDDEN_FOR_MOBILE)
495 638 @RequestParam(required = false) Boolean mobile,
  639 + @ApiParam(value = DASHBOARD_TEXT_SEARCH_DESCRIPTION)
496 640 @RequestParam(required = false) String textSearch,
  641 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DASHBOARD_SORT_PROPERTY_ALLOWABLE_VALUES)
497 642 @RequestParam(required = false) String sortProperty,
  643 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
498 644 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
499   - checkParameter("customerId", strCustomerId);
  645 + checkParameter(CUSTOMER_ID, strCustomerId);
500 646 try {
501 647 TenantId tenantId = getCurrentUser().getTenantId();
502 648 CustomerId customerId = new CustomerId(toUUID(strCustomerId));
503 649 checkCustomerId(customerId, Operation.READ);
504 650 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
505   - if (mobile != null && mobile.booleanValue()) {
  651 + if (mobile != null && mobile) {
506 652 return checkNotNull(dashboardService.findMobileDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
507 653 } else {
508 654 return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink));
... ... @@ -512,6 +658,12 @@ public class DashboardController extends BaseController {
512 658 }
513 659 }
514 660
  661 + @ApiOperation(value = "Get Home Dashboard (getHomeDashboard)",
  662 + notes = "Returns the home dashboard object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the User. " +
  663 + "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. " +
  664 + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. "
  665 + + DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  666 + produces = MediaType.APPLICATION_JSON_VALUE)
515 667 @PreAuthorize("isAuthenticated()")
516 668 @RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
517 669 @ResponseBody
... ... @@ -543,6 +695,12 @@ public class DashboardController extends BaseController {
543 695 }
544 696 }
545 697
  698 + @ApiOperation(value = "Get Home Dashboard Info (getHomeDashboardInfo)",
  699 + notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the User. " +
  700 + "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. " +
  701 + "If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " +
  702 + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  703 + produces = MediaType.APPLICATION_JSON_VALUE)
546 704 @PreAuthorize("isAuthenticated()")
547 705 @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET)
548 706 @ResponseBody
... ... @@ -574,6 +732,10 @@ public class DashboardController extends BaseController {
574 732 }
575 733 }
576 734
  735 + @ApiOperation(value = "Get Tenant Home Dashboard Info (getTenantHomeDashboardInfo)",
  736 + notes = "Returns the home dashboard info object that is configured as 'homeDashboardId' parameter in the 'additionalInfo' of the corresponding tenant. " +
  737 + TENANT_AUTHORITY_PARAGRAPH,
  738 + produces = MediaType.APPLICATION_JSON_VALUE)
577 739 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
578 740 @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
579 741 @ResponseBody
... ... @@ -596,10 +758,16 @@ public class DashboardController extends BaseController {
596 758 }
597 759 }
598 760
  761 + @ApiOperation(value = "Update Tenant Home Dashboard Info (getTenantHomeDashboardInfo)",
  762 + notes = "Update the home dashboard assignment for the current tenant. " +
  763 + TENANT_AUTHORITY_PARAGRAPH,
  764 + produces = MediaType.APPLICATION_JSON_VALUE)
599 765 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
600 766 @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST)
601 767 @ResponseStatus(value = HttpStatus.OK)
602   - public void setTenantHomeDashboardInfo(@RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException {
  768 + public void setTenantHomeDashboardInfo(
  769 + @ApiParam(value = "A JSON object that represents home dashboard id and other parameters", required = true)
  770 + @RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException {
603 771 try {
604 772 if (homeDashboardInfo.getDashboardId() != null) {
605 773 checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ);
... ... @@ -635,7 +803,8 @@ public class DashboardController extends BaseController {
635 803 }
636 804 return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
637 805 }
638   - } catch (Exception e) {}
  806 + } catch (Exception e) {
  807 + }
639 808 return null;
640 809 }
641 810
... ... @@ -651,10 +820,19 @@ public class DashboardController extends BaseController {
651 820 }
652 821 return new HomeDashboard(dashboard, hideDashboardToolbar);
653 822 }
654   - } catch (Exception e) {}
  823 + } catch (Exception e) {
  824 + }
655 825 return null;
656 826 }
657 827
  828 + @ApiOperation(value = "Assign dashboard to edge (assignDashboardToEdge)",
  829 + notes = "Creates assignment of an existing dashboard to an instance of The Edge. " +
  830 + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
  831 + "Second, remote edge service will receive a copy of assignment dashboard " +
  832 + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " +
  833 + "Third, once dashboard will be delivered to edge service, it's going to be available for usage on remote edge instance." +
  834 + TENANT_AUTHORITY_PARAGRAPH,
  835 + produces = MediaType.APPLICATION_JSON_VALUE)
658 836 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
659 837 @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.POST)
660 838 @ResponseBody
... ... @@ -688,6 +866,14 @@ public class DashboardController extends BaseController {
688 866 }
689 867 }
690 868
  869 + @ApiOperation(value = "Unassign dashboard from edge (unassignDashboardFromEdge)",
  870 + notes = "Clears assignment of the dashboard to the edge. " +
  871 + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
  872 + "Second, remote edge service will receive an 'unassign' command to remove dashboard " +
  873 + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " +
  874 + "Third, once 'unassign' command will be delivered to edge service, it's going to remove dashboard locally." +
  875 + TENANT_AUTHORITY_PARAGRAPH,
  876 + produces = MediaType.APPLICATION_JSON_VALUE)
691 877 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
692 878 @RequestMapping(value = "/edge/{edgeId}/dashboard/{dashboardId}", method = RequestMethod.DELETE)
693 879 @ResponseBody
... ...
... ... @@ -19,9 +19,12 @@ import com.google.common.util.concurrent.FutureCallback;
19 19 import com.google.common.util.concurrent.Futures;
20 20 import com.google.common.util.concurrent.ListenableFuture;
21 21 import com.google.common.util.concurrent.MoreExecutors;
  22 +import io.swagger.annotations.ApiOperation;
  23 +import io.swagger.annotations.ApiParam;
22 24 import lombok.RequiredArgsConstructor;
23 25 import lombok.extern.slf4j.Slf4j;
24 26 import org.springframework.http.HttpStatus;
  27 +import org.springframework.http.MediaType;
25 28 import org.springframework.http.ResponseEntity;
26 29 import org.springframework.security.access.prepost.PreAuthorize;
27 30 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -83,6 +86,29 @@ import java.util.List;
83 86 import java.util.UUID;
84 87 import java.util.stream.Collectors;
85 88
  89 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH;
  90 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
  91 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
  92 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION;
  93 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_NAME_DESCRIPTION;
  94 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
  95 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES;
  96 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION;
  97 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION;
  98 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
  99 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
  100 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
  101 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
  102 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
  103 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  104 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  105 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  106 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  107 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  108 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  109 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  110 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
  111 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
86 112 import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
87 113
88 114 @RestController
... ... @@ -91,16 +117,23 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
91 117 @RequiredArgsConstructor
92 118 @Slf4j
93 119 public class DeviceController extends BaseController {
94   - private final DeviceBulkImportService deviceBulkImportService;
95 120
96   - private static final String DEVICE_ID = "deviceId";
97   - private static final String DEVICE_NAME = "deviceName";
98   - private static final String TENANT_ID = "tenantId";
  121 + protected static final String DEVICE_ID = "deviceId";
  122 + protected static final String DEVICE_NAME = "deviceName";
  123 + protected static final String TENANT_ID = "tenantId";
  124 +
  125 + private final DeviceBulkImportService deviceBulkImportService;
99 126
  127 + @ApiOperation(value = "Get Device (getDeviceById)",
  128 + notes = "Fetch the Device object based on the provided Device Id. " +
  129 + "If the user has the authority of 'TENANT_ADMIN', the server checks that the device is owned by the same tenant. " +
  130 + "If the user has the authority of 'CUSTOMER_USER', the server checks that the device is assigned to the same customer." +
  131 + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
100 132 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
101 133 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.GET)
102 134 @ResponseBody
103   - public Device getDeviceById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  135 + public Device getDeviceById(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
  136 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
104 137 checkParameter(DEVICE_ID, strDeviceId);
105 138 try {
106 139 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
... ... @@ -110,10 +143,16 @@ public class DeviceController extends BaseController {
110 143 }
111 144 }
112 145
  146 + @ApiOperation(value = "Get Device Info (getDeviceInfoById)",
  147 + notes = "Fetch the Device Info object based on the provided Device Id. " +
  148 + "If the user has the authority of 'Tenant Administrator', the server checks that the device is owned by the same tenant. " +
  149 + "If the user has the authority of 'Customer User', the server checks that the device is assigned to the same customer. " +
  150 + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
113 151 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
114 152 @RequestMapping(value = "/device/info/{deviceId}", method = RequestMethod.GET)
115 153 @ResponseBody
116   - public DeviceInfo getDeviceInfoById(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  154 + public DeviceInfo getDeviceInfoById(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
  155 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
117 156 checkParameter(DEVICE_ID, strDeviceId);
118 157 try {
119 158 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
... ... @@ -123,11 +162,20 @@ public class DeviceController extends BaseController {
123 162 }
124 163 }
125 164
  165 + @ApiOperation(value = "Create Or Update Device (saveDevice)",
  166 + 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). " +
  167 + "Device credentials are also generated if not provided in the 'accessToken' request parameter. " +
  168 + "The newly created device id will be present in the response. " +
  169 + "Specify existing Device id to update the device. " +
  170 + "Referencing non-existing device Id will cause 'Not Found' error." +
  171 + "\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."
  172 + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
126 173 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
127 174 @RequestMapping(value = "/device", method = RequestMethod.POST)
128 175 @ResponseBody
129   - public Device saveDevice(@RequestBody Device device,
130   - @RequestParam(name = "accessToken", required = false) String accessToken) throws ThingsboardException {
  176 + public Device saveDevice(@ApiParam(value = "A JSON value representing the device.") @RequestBody Device device,
  177 + @ApiParam(value = "Optional value of the device credentials to be used during device creation. " +
  178 + "If omitted, access token will be auto-generated.") @RequestParam(name = "accessToken", required = false) String accessToken) throws ThingsboardException {
131 179 boolean created = device.getId() == null;
132 180 try {
133 181 device.setTenantId(getCurrentUser().getTenantId());
... ... @@ -141,7 +189,7 @@ public class DeviceController extends BaseController {
141 189
142 190 Device savedDevice = checkNotNull(deviceService.saveDeviceWithAccessToken(device, accessToken));
143 191
144   - onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created);
  192 + onDeviceCreatedOrUpdated(savedDevice, oldDevice, !created, getCurrentUser());
145 193
146 194 return savedDevice;
147 195 } catch (Exception e) {
... ... @@ -152,11 +200,11 @@ public class DeviceController extends BaseController {
152 200
153 201 }
154 202
155   - private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated) {
  203 + private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated, SecurityUser user) {
156 204 tbClusterService.onDeviceUpdated(savedDevice, oldDevice);
157 205
158 206 try {
159   - logEntityAction(savedDevice.getId(), savedDevice,
  207 + logEntityAction(user, savedDevice.getId(), savedDevice,
160 208 savedDevice.getCustomerId(),
161 209 updated ? ActionType.UPDATED : ActionType.ADDED, null);
162 210 } catch (ThingsboardException e) {
... ... @@ -164,10 +212,13 @@ public class DeviceController extends BaseController {
164 212 }
165 213 }
166 214
  215 + @ApiOperation(value = "Delete device (deleteDevice)",
  216 + 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)
167 217 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
168 218 @RequestMapping(value = "/device/{deviceId}", method = RequestMethod.DELETE)
169 219 @ResponseStatus(value = HttpStatus.OK)
170   - public void deleteDevice(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  220 + public void deleteDevice(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
  221 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
171 222 checkParameter(DEVICE_ID, strDeviceId);
172 223 try {
173 224 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
... ... @@ -193,10 +244,14 @@ public class DeviceController extends BaseController {
193 244 }
194 245 }
195 246
  247 + @ApiOperation(value = "Assign device to customer (assignDeviceToCustomer)",
  248 + notes = "Creates assignment of the device to customer. Customer will be able to query device afterwards." + TENANT_AUTHORITY_PARAGRAPH)
196 249 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
197 250 @RequestMapping(value = "/customer/{customerId}/device/{deviceId}", method = RequestMethod.POST)
198 251 @ResponseBody
199   - public Device assignDeviceToCustomer(@PathVariable("customerId") String strCustomerId,
  252 + public Device assignDeviceToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
  253 + @PathVariable("customerId") String strCustomerId,
  254 + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
200 255 @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
201 256 checkParameter("customerId", strCustomerId);
202 257 checkParameter(DEVICE_ID, strDeviceId);
... ... @@ -225,10 +280,13 @@ public class DeviceController extends BaseController {
225 280 }
226 281 }
227 282
  283 + @ApiOperation(value = "Unassign device from customer (unassignDeviceFromCustomer)",
  284 + notes = "Clears assignment of the device to customer. Customer will not be able to query device afterwards." + TENANT_AUTHORITY_PARAGRAPH)
228 285 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
229 286 @RequestMapping(value = "/customer/device/{deviceId}", method = RequestMethod.DELETE)
230 287 @ResponseBody
231   - public Device unassignDeviceFromCustomer(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  288 + public Device unassignDeviceFromCustomer(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
  289 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
232 290 checkParameter(DEVICE_ID, strDeviceId);
233 291 try {
234 292 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
... ... @@ -256,10 +314,15 @@ public class DeviceController extends BaseController {
256 314 }
257 315 }
258 316
  317 + @ApiOperation(value = "Make device publicly available (assignDeviceToPublicCustomer)",
  318 + notes = "Device will be available for non-authorized (not logged-in) users. " +
  319 + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
  320 + "However, users that are logged-in and belong to different tenant will not be able to access the device." + TENANT_AUTHORITY_PARAGRAPH)
259 321 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
260 322 @RequestMapping(value = "/customer/public/device/{deviceId}", method = RequestMethod.POST)
261 323 @ResponseBody
262   - public Device assignDeviceToPublicCustomer(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  324 + public Device assignDeviceToPublicCustomer(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
  325 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
263 326 checkParameter(DEVICE_ID, strDeviceId);
264 327 try {
265 328 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
... ... @@ -280,10 +343,13 @@ public class DeviceController extends BaseController {
280 343 }
281 344 }
282 345
  346 + @ApiOperation(value = "Get Device Credentials (getDeviceCredentialsByDeviceId)",
  347 + notes = "If during device creation there wasn't specified any credentials, platform generates random 'ACCESS_TOKEN' credentials." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
283 348 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
284 349 @RequestMapping(value = "/device/{deviceId}/credentials", method = RequestMethod.GET)
285 350 @ResponseBody
286   - public DeviceCredentials getDeviceCredentialsByDeviceId(@PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
  351 + public DeviceCredentials getDeviceCredentialsByDeviceId(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
  352 + @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
287 353 checkParameter(DEVICE_ID, strDeviceId);
288 354 try {
289 355 DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
... ... @@ -301,10 +367,16 @@ public class DeviceController extends BaseController {
301 367 }
302 368 }
303 369
  370 + @ApiOperation(value = "Update device credentials (updateDeviceCredentials)", notes = "During device creation, platform generates random 'ACCESS_TOKEN' credentials. " +
  371 + "Use this method to update the device credentials. First use 'getDeviceCredentialsByDeviceId' to get the credentials id and value. " +
  372 + "Then use current method to update the credentials type and value. It is not possible to create multiple device credentials for the same device. " +
  373 + "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)
304 374 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
305 375 @RequestMapping(value = "/device/credentials", method = RequestMethod.POST)
306 376 @ResponseBody
307   - public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
  377 + public DeviceCredentials updateDeviceCredentials(
  378 + @ApiParam(value = "A JSON value representing the device credentials.")
  379 + @RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException {
308 380 checkNotNull(deviceCredentials);
309 381 try {
310 382 Device device = checkDeviceId(deviceCredentials.getDeviceId(), Operation.WRITE_CREDENTIALS);
... ... @@ -325,15 +397,24 @@ public class DeviceController extends BaseController {
325 397 }
326 398 }
327 399
  400 + @ApiOperation(value = "Get Tenant Devices (getTenantDevices)",
  401 + notes = "Returns a page of devices owned by tenant. " +
  402 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
328 403 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
329 404 @RequestMapping(value = "/tenant/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
330 405 @ResponseBody
331 406 public PageData<Device> getTenantDevices(
  407 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
332 408 @RequestParam int pageSize,
  409 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
333 410 @RequestParam int page,
  411 + @ApiParam(value = DEVICE_TYPE_DESCRIPTION)
334 412 @RequestParam(required = false) String type,
  413 + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
335 414 @RequestParam(required = false) String textSearch,
  415 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
336 416 @RequestParam(required = false) String sortProperty,
  417 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
337 418 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
338 419 try {
339 420 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -348,17 +429,28 @@ public class DeviceController extends BaseController {
348 429 }
349 430 }
350 431
  432 + @ApiOperation(value = "Get Tenant Device Infos (getTenantDeviceInfos)",
  433 + notes = "Returns a page of devices info objects owned by tenant. " +
  434 + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH)
351 435 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
352 436 @RequestMapping(value = "/tenant/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
353 437 @ResponseBody
354 438 public PageData<DeviceInfo> getTenantDeviceInfos(
  439 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
355 440 @RequestParam int pageSize,
  441 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
356 442 @RequestParam int page,
  443 + @ApiParam(value = DEVICE_TYPE_DESCRIPTION)
357 444 @RequestParam(required = false) String type,
  445 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
358 446 @RequestParam(required = false) String deviceProfileId,
  447 + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
359 448 @RequestParam(required = false) String textSearch,
  449 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
360 450 @RequestParam(required = false) String sortProperty,
361   - @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  451 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
  452 + @RequestParam(required = false) String sortOrder
  453 + ) throws ThingsboardException {
362 454 try {
363 455 TenantId tenantId = getCurrentUser().getTenantId();
364 456 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
... ... @@ -375,10 +467,14 @@ public class DeviceController extends BaseController {
375 467 }
376 468 }
377 469
  470 + @ApiOperation(value = "Get Tenant Device (getTenantDevice)",
  471 + notes = "Requested device must be owned by tenant that the user belongs to. " +
  472 + "Device name is an unique property of device. So it can be used to identify the device." + TENANT_AUTHORITY_PARAGRAPH)
378 473 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
379 474 @RequestMapping(value = "/tenant/devices", params = {"deviceName"}, method = RequestMethod.GET)
380 475 @ResponseBody
381 476 public Device getTenantDevice(
  477 + @ApiParam(value = DEVICE_NAME_DESCRIPTION)
382 478 @RequestParam String deviceName) throws ThingsboardException {
383 479 try {
384 480 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -388,16 +484,26 @@ public class DeviceController extends BaseController {
388 484 }
389 485 }
390 486
  487 + @ApiOperation(value = "Get Customer Devices (getCustomerDevices)",
  488 + notes = "Returns a page of devices objects assigned to customer. " +
  489 + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
391 490 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
392 491 @RequestMapping(value = "/customer/{customerId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
393 492 @ResponseBody
394 493 public PageData<Device> getCustomerDevices(
  494 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
395 495 @PathVariable("customerId") String strCustomerId,
  496 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
396 497 @RequestParam int pageSize,
  498 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
397 499 @RequestParam int page,
  500 + @ApiParam(value = DEVICE_TYPE_DESCRIPTION)
398 501 @RequestParam(required = false) String type,
  502 + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
399 503 @RequestParam(required = false) String textSearch,
  504 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
400 505 @RequestParam(required = false) String sortProperty,
  506 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
401 507 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
402 508 checkParameter("customerId", strCustomerId);
403 509 try {
... ... @@ -415,17 +521,28 @@ public class DeviceController extends BaseController {
415 521 }
416 522 }
417 523
  524 + @ApiOperation(value = "Get Customer Device Infos (getCustomerDeviceInfos)",
  525 + notes = "Returns a page of devices info objects assigned to customer. " +
  526 + PAGE_DATA_PARAMETERS + DEVICE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
418 527 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
419 528 @RequestMapping(value = "/customer/{customerId}/deviceInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
420 529 @ResponseBody
421 530 public PageData<DeviceInfo> getCustomerDeviceInfos(
  531 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
422 532 @PathVariable("customerId") String strCustomerId,
  533 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
423 534 @RequestParam int pageSize,
  535 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
424 536 @RequestParam int page,
  537 + @ApiParam(value = DEVICE_TYPE_DESCRIPTION)
425 538 @RequestParam(required = false) String type,
  539 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
426 540 @RequestParam(required = false) String deviceProfileId,
  541 + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
427 542 @RequestParam(required = false) String textSearch,
  543 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
428 544 @RequestParam(required = false) String sortProperty,
  545 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
429 546 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
430 547 checkParameter("customerId", strCustomerId);
431 548 try {
... ... @@ -446,10 +563,13 @@ public class DeviceController extends BaseController {
446 563 }
447 564 }
448 565
  566 + @ApiOperation(value = "Get Devices By Ids (getDevicesByIds)",
  567 + notes = "Requested devices must be owned by tenant or assigned to customer which user is performing the request. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
449 568 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
450 569 @RequestMapping(value = "/devices", params = {"deviceIds"}, method = RequestMethod.GET)
451 570 @ResponseBody
452 571 public List<Device> getDevicesByIds(
  572 + @ApiParam(value = "A list of devices ids, separated by comma ','")
453 573 @RequestParam("deviceIds") String[] strDeviceIds) throws ThingsboardException {
454 574 checkArrayParameter("deviceIds", strDeviceIds);
455 575 try {
... ... @@ -472,10 +592,16 @@ public class DeviceController extends BaseController {
472 592 }
473 593 }
474 594
  595 + @ApiOperation(value = "Find related devices (findByQuery)",
  596 + notes = "Returns all devices that are related to the specific entity. " +
  597 + "The entity id, relation type, device types, depth of the search, and other query parameters defined using complex 'DeviceSearchQuery' object. " +
  598 + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
475 599 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
476 600 @RequestMapping(value = "/devices", method = RequestMethod.POST)
477 601 @ResponseBody
478   - public List<Device> findByQuery(@RequestBody DeviceSearchQuery query) throws ThingsboardException {
  602 + public List<Device> findByQuery(
  603 + @ApiParam(value = "The device search query JSON")
  604 + @RequestBody DeviceSearchQuery query) throws ThingsboardException {
479 605 checkNotNull(query);
480 606 checkNotNull(query.getParameters());
481 607 checkNotNull(query.getDeviceTypes());
... ... @@ -496,6 +622,9 @@ public class DeviceController extends BaseController {
496 622 }
497 623 }
498 624
  625 + @ApiOperation(value = "Get Device Types (getDeviceTypes)",
  626 + 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."
  627 + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
499 628 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
500 629 @RequestMapping(value = "/device/types", method = RequestMethod.GET)
501 630 @ResponseBody
... ... @@ -510,10 +639,19 @@ public class DeviceController extends BaseController {
510 639 }
511 640 }
512 641
  642 + @ApiOperation(value = "Claim device (claimDevice)",
  643 + notes = "Claiming makes it possible to assign a device to the specific customer using device/server side claiming data (in the form of secret key)." +
  644 + "To make this happen you have to provide unique device name and optional claiming data (it is needed only for device-side claiming)." +
  645 + "Once device is claimed, the customer becomes its owner and customer users may access device data as well as control the device. \n" +
  646 + "In order to enable claiming devices feature a system parameter security.claim.allowClaimingByDefault should be set to true, " +
  647 + "otherwise a server-side claimingAllowed attribute with the value true is obligatory for provisioned devices. \n" +
  648 + "See official documentation for more details regarding claiming." + CUSTOMER_AUTHORITY_PARAGRAPH)
513 649 @PreAuthorize("hasAuthority('CUSTOMER_USER')")
514 650 @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.POST)
515 651 @ResponseBody
516   - public DeferredResult<ResponseEntity> claimDevice(@PathVariable(DEVICE_NAME) String deviceName,
  652 + public DeferredResult<ResponseEntity> claimDevice(@ApiParam(value = "Unique name of the device which is going to be claimed")
  653 + @PathVariable(DEVICE_NAME) String deviceName,
  654 + @ApiParam(value = "Claiming request which can optionally contain secret key")
517 655 @RequestBody(required = false) ClaimRequest claimRequest) throws ThingsboardException {
518 656 checkParameter(DEVICE_NAME, deviceName);
519 657 try {
... ... @@ -564,10 +702,14 @@ public class DeviceController extends BaseController {
564 702 }
565 703 }
566 704
  705 + @ApiOperation(value = "Reclaim device (reClaimDevice)",
  706 + notes = "Reclaiming means the device will be unassigned from the customer and the device will be available for claiming again."
  707 + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
567 708 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
568 709 @RequestMapping(value = "/customer/device/{deviceName}/claim", method = RequestMethod.DELETE)
569 710 @ResponseStatus(value = HttpStatus.OK)
570   - public DeferredResult<ResponseEntity> reClaimDevice(@PathVariable(DEVICE_NAME) String deviceName) throws ThingsboardException {
  711 + public DeferredResult<ResponseEntity> reClaimDevice(@ApiParam(value = "Unique name of the device which is going to be reclaimed")
  712 + @PathVariable(DEVICE_NAME) String deviceName) throws ThingsboardException {
571 713 checkParameter(DEVICE_NAME, deviceName);
572 714 try {
573 715 final DeferredResult<ResponseEntity> deferredResult = new DeferredResult<>();
... ... @@ -615,10 +757,14 @@ public class DeviceController extends BaseController {
615 757 return DataConstants.DEFAULT_SECRET_KEY;
616 758 }
617 759
  760 + @ApiOperation(value = "Assign device to tenant (assignDeviceToTenant)",
  761 + notes = "Creates assignment of the device to tenant. Thereafter tenant will be able to reassign the device to a customer." + TENANT_AUTHORITY_PARAGRAPH)
618 762 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
619 763 @RequestMapping(value = "/tenant/{tenantId}/device/{deviceId}", method = RequestMethod.POST)
620 764 @ResponseBody
621   - public Device assignDeviceToTenant(@PathVariable(TENANT_ID) String strTenantId,
  765 + public Device assignDeviceToTenant(@ApiParam(value = TENANT_ID_PARAM_DESCRIPTION)
  766 + @PathVariable(TENANT_ID) String strTenantId,
  767 + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
622 768 @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
623 769 checkParameter(TENANT_ID, strTenantId);
624 770 checkParameter(DEVICE_ID, strDeviceId);
... ... @@ -665,10 +811,19 @@ public class DeviceController extends BaseController {
665 811 return metaData;
666 812 }
667 813
  814 + @ApiOperation(value = "Assign device to edge (assignDeviceToEdge)",
  815 + notes = "Creates assignment of an existing device to an instance of The Edge. " +
  816 + EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
  817 + "Second, remote edge service will receive a copy of assignment device " +
  818 + EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION + ". " +
  819 + "Third, once device will be delivered to edge service, it's going to be available for usage on remote edge instance." + TENANT_AUTHORITY_PARAGRAPH,
  820 + produces = MediaType.APPLICATION_JSON_VALUE)
668 821 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
669 822 @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.POST)
670 823 @ResponseBody
671   - public Device assignDeviceToEdge(@PathVariable(EDGE_ID) String strEdgeId,
  824 + public Device assignDeviceToEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION)
  825 + @PathVariable(EDGE_ID) String strEdgeId,
  826 + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
672 827 @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
673 828 checkParameter(EDGE_ID, strEdgeId);
674 829 checkParameter(DEVICE_ID, strDeviceId);
... ... @@ -699,10 +854,19 @@ public class DeviceController extends BaseController {
699 854 }
700 855 }
701 856
  857 + @ApiOperation(value = "Unassign device from edge (unassignDeviceFromEdge)",
  858 + notes = "Clears assignment of the device to the edge. " +
  859 + EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION +
  860 + "Second, remote edge service will receive an 'unassign' command to remove device " +
  861 + EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION + ". " +
  862 + "Third, once 'unassign' command will be delivered to edge service, it's going to remove device locally." + TENANT_AUTHORITY_PARAGRAPH,
  863 + produces = MediaType.APPLICATION_JSON_VALUE)
702 864 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
703 865 @RequestMapping(value = "/edge/{edgeId}/device/{deviceId}", method = RequestMethod.DELETE)
704 866 @ResponseBody
705   - public Device unassignDeviceFromEdge(@PathVariable(EDGE_ID) String strEdgeId,
  867 + public Device unassignDeviceFromEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION)
  868 + @PathVariable(EDGE_ID) String strEdgeId,
  869 + @ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
706 870 @PathVariable(DEVICE_ID) String strDeviceId) throws ThingsboardException {
707 871 checkParameter(EDGE_ID, strEdgeId);
708 872 checkParameter(DEVICE_ID, strDeviceId);
... ... @@ -733,18 +897,30 @@ public class DeviceController extends BaseController {
733 897 }
734 898 }
735 899
  900 + @ApiOperation(value = "Get devices assigned to edge (getEdgeDevices)",
  901 + notes = "Returns a page of devices assigned to edge. " +
  902 + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
736 903 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
737 904 @RequestMapping(value = "/edge/{edgeId}/devices", params = {"pageSize", "page"}, method = RequestMethod.GET)
738 905 @ResponseBody
739 906 public PageData<Device> getEdgeDevices(
  907 + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
740 908 @PathVariable(EDGE_ID) String strEdgeId,
  909 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
741 910 @RequestParam int pageSize,
  911 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
742 912 @RequestParam int page,
  913 + @ApiParam(value = DEVICE_TYPE_DESCRIPTION)
743 914 @RequestParam(required = false) String type,
  915 + @ApiParam(value = DEVICE_TEXT_SEARCH_DESCRIPTION)
744 916 @RequestParam(required = false) String textSearch,
  917 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES)
745 918 @RequestParam(required = false) String sortProperty,
  919 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
746 920 @RequestParam(required = false) String sortOrder,
  921 + @ApiParam(value = "Timestamp. Devices with creation time before it won't be queried")
747 922 @RequestParam(required = false) Long startTime,
  923 + @ApiParam(value = "Timestamp. Devices with creation time after it won't be queried")
748 924 @RequestParam(required = false) Long endTime) throws ThingsboardException {
749 925 checkParameter(EDGE_ID, strEdgeId);
750 926 try {
... ... @@ -776,10 +952,17 @@ public class DeviceController extends BaseController {
776 952 }
777 953 }
778 954
  955 + @ApiOperation(value = "Count devices by device profile (countByDeviceProfileAndEmptyOtaPackage)",
  956 + notes = "The platform gives an ability to load OTA (over-the-air) packages to devices. " +
  957 + "It can be done in two different ways: device scope or device profile scope." +
  958 + "In the response you will find the number of devices with specified device profile, but without previously defined device scope OTA package. " +
  959 + "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)
779 960 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
780 961 @RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET)
781 962 @ResponseBody
782   - public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType,
  963 + public Long countByDeviceProfileAndEmptyOtaPackage(@ApiParam(value = "OTA package type", allowableValues = "FIRMWARE, SOFTWARE")
  964 + @PathVariable("otaPackageType") String otaPackageType,
  965 + @ApiParam(value = "Device Profile Id. I.g. '784f394c-42b6-435a-983c-b7beff2784f9'")
783 966 @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException {
784 967 checkParameter("OtaPackageType", otaPackageType);
785 968 checkParameter("DeviceProfileId", deviceProfileId);
... ... @@ -793,11 +976,14 @@ public class DeviceController extends BaseController {
793 976 }
794 977 }
795 978
  979 + @ApiOperation(value = "Import the bulk of devices (processDevicesBulkImport)",
  980 + notes = "There's an ability to import the bulk of devices using the only .csv file." + TENANT_AUTHORITY_PARAGRAPH)
796 981 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
797 982 @PostMapping("/device/bulk_import")
798 983 public BulkImportResult<Device> processDevicesBulkImport(@RequestBody BulkImportRequest request) throws Exception {
799   - return deviceBulkImportService.processBulkImport(request, getCurrentUser(), importedDeviceInfo -> {
800   - onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated());
  984 + SecurityUser user = getCurrentUser();
  985 + return deviceBulkImportService.processBulkImport(request, user, importedDeviceInfo -> {
  986 + onDeviceCreatedOrUpdated(importedDeviceInfo.getEntity(), importedDeviceInfo.getOldEntity(), importedDeviceInfo.isUpdated(), user);
801 987 });
802 988 }
803 989
... ...
... ... @@ -15,6 +15,8 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import io.swagger.annotations.ApiOperation;
  19 +import io.swagger.annotations.ApiParam;
18 20 import lombok.extern.slf4j.Slf4j;
19 21 import org.apache.commons.lang3.StringUtils;
20 22 import org.springframework.beans.factory.annotation.Autowired;
... ... @@ -47,21 +49,43 @@ import java.util.List;
47 49 import java.util.Objects;
48 50 import java.util.UUID;
49 51
  52 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA;
  53 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID;
  54 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_ID_PARAM_DESCRIPTION;
  55 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_INFO_DESCRIPTION;
  56 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES;
  57 +import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION;
  58 +import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
  59 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  60 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  61 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  62 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  63 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  64 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  65 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  66 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  67 +import static org.thingsboard.server.controller.ControllerConstants.TRANSPORT_TYPE_ALLOWABLE_VALUES;
  68 +
50 69 @RestController
51 70 @TbCoreComponent
52 71 @RequestMapping("/api")
53 72 @Slf4j
54 73 public class DeviceProfileController extends BaseController {
55 74
56   - private static final String DEVICE_PROFILE_ID = "deviceProfileId";
57 75
58 76 @Autowired
59 77 private TimeseriesService timeseriesService;
60 78
  79 + @ApiOperation(value = "Get Device Profile (getDeviceProfileById)",
  80 + notes = "Fetch the Device Profile object based on the provided Device Profile Id. " +
  81 + "The server checks that the device profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH,
  82 + produces = "application/json")
61 83 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
62 84 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET)
63 85 @ResponseBody
64   - public DeviceProfile getDeviceProfileById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  86 + public DeviceProfile getDeviceProfileById(
  87 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
  88 + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
65 89 checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
66 90 try {
67 91 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
... ... @@ -71,10 +95,16 @@ public class DeviceProfileController extends BaseController {
71 95 }
72 96 }
73 97
  98 + @ApiOperation(value = "Get Device Profile Info (getDeviceProfileInfoById)",
  99 + notes = "Fetch the Device Profile Info object based on the provided Device Profile Id. "
  100 + + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  101 + produces = "application/json")
74 102 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
75 103 @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET)
76 104 @ResponseBody
77   - public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  105 + public DeviceProfileInfo getDeviceProfileInfoById(
  106 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
  107 + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
78 108 checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
79 109 try {
80 110 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
... ... @@ -84,6 +114,10 @@ public class DeviceProfileController extends BaseController {
84 114 }
85 115 }
86 116
  117 + @ApiOperation(value = "Get Default Device Profile (getDefaultDeviceProfileInfo)",
  118 + notes = "Fetch the Default Device Profile Info object. " +
  119 + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  120 + produces = "application/json")
87 121 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
88 122 @RequestMapping(value = "/deviceProfileInfo/default", method = RequestMethod.GET)
89 123 @ResponseBody
... ... @@ -95,10 +129,18 @@ public class DeviceProfileController extends BaseController {
95 129 }
96 130 }
97 131
  132 + @ApiOperation(value = "Get time-series keys (getTimeseriesKeys)",
  133 + notes = "Get a set of unique time-series keys used by devices that belong to specified profile. " +
  134 + "If profile is not set returns a list of unique keys among all profiles. " +
  135 + "The call is used for auto-complete in the UI forms. " +
  136 + "The implementation limits the number of devices that participate in search to 100 as a trade of between accurate results and time-consuming queries. " +
  137 + TENANT_AUTHORITY_PARAGRAPH,
  138 + produces = "application/json")
98 139 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
99 140 @RequestMapping(value = "/deviceProfile/devices/keys/timeseries", method = RequestMethod.GET)
100 141 @ResponseBody
101 142 public List<String> getTimeseriesKeys(
  143 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
102 144 @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
103 145 DeviceProfileId deviceProfileId;
104 146 if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
... ... @@ -115,10 +157,18 @@ public class DeviceProfileController extends BaseController {
115 157 }
116 158 }
117 159
  160 + @ApiOperation(value = "Get attribute keys (getAttributesKeys)",
  161 + notes = "Get a set of unique attribute keys used by devices that belong to specified profile. " +
  162 + "If profile is not set returns a list of unique keys among all profiles. " +
  163 + "The call is used for auto-complete in the UI forms. " +
  164 + "The implementation limits the number of devices that participate in search to 100 as a trade of between accurate results and time-consuming queries. " +
  165 + TENANT_AUTHORITY_PARAGRAPH,
  166 + produces = "application/json")
118 167 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
119 168 @RequestMapping(value = "/deviceProfile/devices/keys/attributes", method = RequestMethod.GET)
120 169 @ResponseBody
121 170 public List<String> getAttributesKeys(
  171 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
122 172 @RequestParam(name = DEVICE_PROFILE_ID, required = false) String deviceProfileIdStr) throws ThingsboardException {
123 173 DeviceProfileId deviceProfileId;
124 174 if (StringUtils.isNotEmpty(deviceProfileIdStr)) {
... ... @@ -135,10 +185,21 @@ public class DeviceProfileController extends BaseController {
135 185 }
136 186 }
137 187
  188 + @ApiOperation(value = "Create Or Update Device Profile (saveDeviceProfile)",
  189 + notes = "Create or update the Device Profile. When creating device profile, platform generates device profile id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
  190 + "The newly created device profile id will be present in the response. " +
  191 + "Specify existing device profile id to update the device profile. " +
  192 + "Referencing non-existing device profile Id will cause 'Not Found' error. " + NEW_LINE +
  193 + "Device profile name is unique in the scope of tenant. Only one 'default' device profile may exist in scope of tenant." + DEVICE_PROFILE_DATA +
  194 + TENANT_AUTHORITY_PARAGRAPH,
  195 + produces = "application/json",
  196 + consumes = "application/json")
138 197 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
139 198 @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST)
140 199 @ResponseBody
141   - public DeviceProfile saveDeviceProfile(@RequestBody DeviceProfile deviceProfile) throws ThingsboardException {
  200 + public DeviceProfile saveDeviceProfile(
  201 + @ApiParam(value = "A JSON value representing the device profile.")
  202 + @RequestBody DeviceProfile deviceProfile) throws ThingsboardException {
142 203 try {
143 204 boolean created = deviceProfile.getId() == null;
144 205 deviceProfile.setTenantId(getTenantId());
... ... @@ -180,10 +241,16 @@ public class DeviceProfileController extends BaseController {
180 241 }
181 242 }
182 243
  244 + @ApiOperation(value = "Delete device profile (deleteDeviceProfile)",
  245 + notes = "Deletes the device profile. Referencing non-existing device profile Id will cause an error. " +
  246 + "Can't delete the device profile if it is referenced by existing devices." + TENANT_AUTHORITY_PARAGRAPH,
  247 + produces = "application/json")
183 248 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
184 249 @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE)
185 250 @ResponseStatus(value = HttpStatus.OK)
186   - public void deleteDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  251 + public void deleteDeviceProfile(
  252 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
  253 + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
187 254 checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
188 255 try {
189 256 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
... ... @@ -207,10 +274,15 @@ public class DeviceProfileController extends BaseController {
207 274 }
208 275 }
209 276
  277 + @ApiOperation(value = "Make Device Profile Default (setDefaultDeviceProfile)",
  278 + notes = "Marks device profile as default within a tenant scope." + TENANT_AUTHORITY_PARAGRAPH,
  279 + produces = "application/json")
210 280 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
211 281 @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST)
212 282 @ResponseBody
213   - public DeviceProfile setDefaultDeviceProfile(@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
  283 + public DeviceProfile setDefaultDeviceProfile(
  284 + @ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
  285 + @PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
214 286 checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
215 287 try {
216 288 DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
... ... @@ -238,14 +310,24 @@ public class DeviceProfileController extends BaseController {
238 310 }
239 311 }
240 312
  313 + @ApiOperation(value = "Get Device Profiles (getDeviceProfiles)",
  314 + notes = "Returns a page of devices profile objects owned by tenant. " +
  315 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
  316 + produces = "application/json")
241 317 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
242 318 @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
243 319 @ResponseBody
244   - public PageData<DeviceProfile> getDeviceProfiles(@RequestParam int pageSize,
245   - @RequestParam int page,
246   - @RequestParam(required = false) String textSearch,
247   - @RequestParam(required = false) String sortProperty,
248   - @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  320 + public PageData<DeviceProfile> getDeviceProfiles(
  321 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
  322 + @RequestParam int pageSize,
  323 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
  324 + @RequestParam int page,
  325 + @ApiParam(value = DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION)
  326 + @RequestParam(required = false) String textSearch,
  327 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
  328 + @RequestParam(required = false) String sortProperty,
  329 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
  330 + @RequestParam(required = false) String sortOrder) throws ThingsboardException {
249 331 try {
250 332 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
251 333 return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink));
... ... @@ -254,15 +336,26 @@ public class DeviceProfileController extends BaseController {
254 336 }
255 337 }
256 338
  339 + @ApiOperation(value = "Get Device Profiles for transport type (getDeviceProfileInfos)",
  340 + notes = "Returns a page of devices profile info objects owned by tenant. " +
  341 + PAGE_DATA_PARAMETERS + DEVICE_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  342 + produces = "application/json")
257 343 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
258 344 @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
259 345 @ResponseBody
260   - public PageData<DeviceProfileInfo> getDeviceProfileInfos(@RequestParam int pageSize,
261   - @RequestParam int page,
262   - @RequestParam(required = false) String textSearch,
263   - @RequestParam(required = false) String sortProperty,
264   - @RequestParam(required = false) String sortOrder,
265   - @RequestParam(required = false) String transportType) throws ThingsboardException {
  346 + public PageData<DeviceProfileInfo> getDeviceProfileInfos(
  347 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
  348 + @RequestParam int pageSize,
  349 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
  350 + @RequestParam int page,
  351 + @ApiParam(value = DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION)
  352 + @RequestParam(required = false) String textSearch,
  353 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
  354 + @RequestParam(required = false) String sortProperty,
  355 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
  356 + @RequestParam(required = false) String sortOrder,
  357 + @ApiParam(value = "Type of the transport", allowableValues = TRANSPORT_TYPE_ALLOWABLE_VALUES)
  358 + @RequestParam(required = false) String transportType) throws ThingsboardException {
266 359 try {
267 360 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
268 361 return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType));
... ...
... ... @@ -17,9 +17,12 @@ package org.thingsboard.server.controller;
17 17
18 18 import com.fasterxml.jackson.databind.JsonNode;
19 19 import com.google.common.util.concurrent.ListenableFuture;
  20 +import io.swagger.annotations.ApiOperation;
  21 +import io.swagger.annotations.ApiParam;
20 22 import lombok.RequiredArgsConstructor;
21 23 import lombok.extern.slf4j.Slf4j;
22 24 import org.springframework.http.HttpStatus;
  25 +import org.springframework.http.MediaType;
23 26 import org.springframework.http.ResponseEntity;
24 27 import org.springframework.security.access.prepost.PreAuthorize;
25 28 import org.springframework.web.bind.annotation.PathVariable;
... ... @@ -66,6 +69,22 @@ import java.util.ArrayList;
66 69 import java.util.List;
67 70 import java.util.stream.Collectors;
68 71
  72 +import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
  73 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
  74 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_INFO_DESCRIPTION;
  75 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_SORT_PROPERTY_ALLOWABLE_VALUES;
  76 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_TEXT_SEARCH_DESCRIPTION;
  77 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_TYPE_DESCRIPTION;
  78 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  79 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  80 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  81 +import static org.thingsboard.server.controller.ControllerConstants.RULE_CHAIN_ID_PARAM_DESCRIPTION;
  82 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  83 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  84 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  85 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
  86 +import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
  87 +
69 88 @RestController
70 89 @TbCoreComponent
71 90 @Slf4j
... ... @@ -75,7 +94,11 @@ public class EdgeController extends BaseController {
75 94 private final EdgeBulkImportService edgeBulkImportService;
76 95
77 96 public static final String EDGE_ID = "edgeId";
  97 + 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. " +
  98 + "If the user has the authority of 'Customer User', the server checks that the edge is assigned to the same customer.";
78 99
  100 + @ApiOperation(value = "Is edges support enabled (isEdgesSupportEnabled)",
  101 + notes = "Returns 'true' if edges support enabled on server, 'false' - otherwise.")
79 102 @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
80 103 @RequestMapping(value = "/edges/enabled", method = RequestMethod.GET)
81 104 @ResponseBody
... ... @@ -83,10 +106,14 @@ public class EdgeController extends BaseController {
83 106 return edgesEnabled;
84 107 }
85 108
  109 + @ApiOperation(value = "Get Edge (getEdgeById)",
  110 + notes = "Get the Edge object based on the provided Edge Id. " + EDGE_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  111 + produces = MediaType.APPLICATION_JSON_VALUE)
86 112 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
87 113 @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.GET)
88 114 @ResponseBody
89   - public Edge getEdgeById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
  115 + public Edge getEdgeById(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  116 + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
90 117 checkParameter(EDGE_ID, strEdgeId);
91 118 try {
92 119 EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
... ... @@ -100,10 +127,14 @@ public class EdgeController extends BaseController {
100 127 }
101 128 }
102 129
  130 + @ApiOperation(value = "Get Edge Info (getEdgeInfoById)",
  131 + notes = "Get the Edge Info object based on the provided Edge Id. " + EDGE_SECURITY_CHECK + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  132 + produces = MediaType.APPLICATION_JSON_VALUE)
103 133 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
104 134 @RequestMapping(value = "/edge/info/{edgeId}", method = RequestMethod.GET)
105 135 @ResponseBody
106   - public EdgeInfo getEdgeInfoById(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
  136 + public EdgeInfo getEdgeInfoById(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  137 + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
107 138 checkParameter(EDGE_ID, strEdgeId);
108 139 try {
109 140 EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
... ... @@ -117,10 +148,19 @@ public class EdgeController extends BaseController {
117 148 }
118 149 }
119 150
  151 + @ApiOperation(value = "Create Or Update Edge (saveEdge)",
  152 + notes = "Create or update the Edge. When creating edge, platform generates Edge Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
  153 + "The newly created edge id will be present in the response. " +
  154 + "Specify existing Edge id to update the edge. " +
  155 + "Referencing non-existing Edge Id will cause 'Not Found' error." +
  156 + "\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."
  157 + + TENANT_AUTHORITY_PARAGRAPH,
  158 + produces = MediaType.APPLICATION_JSON_VALUE)
120 159 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
121 160 @RequestMapping(value = "/edge", method = RequestMethod.POST)
122 161 @ResponseBody
123   - public Edge saveEdge(@RequestBody Edge edge) throws ThingsboardException {
  162 + public Edge saveEdge(@ApiParam(value = "A JSON value representing the edge.", required = true)
  163 + @RequestBody Edge edge) throws ThingsboardException {
124 164 try {
125 165 TenantId tenantId = getCurrentUser().getTenantId();
126 166 edge.setTenantId(tenantId);
... ... @@ -140,7 +180,7 @@ public class EdgeController extends BaseController {
140 180 edge.getId(), edge);
141 181
142 182 Edge savedEdge = checkNotNull(edgeService.saveEdge(edge, true));
143   - onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created);
  183 + onEdgeCreatedOrUpdated(tenantId, savedEdge, edgeTemplateRootRuleChain, !created, getCurrentUser());
144 184
145 185 return savedEdge;
146 186 } catch (Exception e) {
... ... @@ -150,7 +190,7 @@ public class EdgeController extends BaseController {
150 190 }
151 191 }
152 192
153   - private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated) throws IOException, ThingsboardException {
  193 + private void onEdgeCreatedOrUpdated(TenantId tenantId, Edge edge, RuleChain edgeTemplateRootRuleChain, boolean updated, SecurityUser user) throws IOException, ThingsboardException {
154 194 if (!updated) {
155 195 ruleChainService.assignRuleChainToEdge(tenantId, edgeTemplateRootRuleChain.getId(), edge.getId());
156 196 edgeNotificationService.setEdgeRootRuleChain(tenantId, edge, edgeTemplateRootRuleChain.getId());
... ... @@ -160,13 +200,16 @@ public class EdgeController extends BaseController {
160 200 tbClusterService.broadcastEntityStateChangeEvent(edge.getTenantId(), edge.getId(),
161 201 updated ? ComponentLifecycleEvent.UPDATED : ComponentLifecycleEvent.CREATED);
162 202
163   - logEntityAction(edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null);
  203 + logEntityAction(user, edge.getId(), edge, null, updated ? ActionType.UPDATED : ActionType.ADDED, null);
164 204 }
165 205
  206 + @ApiOperation(value = "Delete edge (deleteEdge)",
  207 + notes = "Deletes the edge. Referencing non-existing edge Id will cause an error."+ TENANT_AUTHORITY_PARAGRAPH)
166 208 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
167 209 @RequestMapping(value = "/edge/{edgeId}", method = RequestMethod.DELETE)
168 210 @ResponseStatus(value = HttpStatus.OK)
169   - public void deleteEdge(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
  211 + public void deleteEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  212 + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
170 213 checkParameter(EDGE_ID, strEdgeId);
171 214 try {
172 215 EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
... ... @@ -191,13 +234,21 @@ public class EdgeController extends BaseController {
191 234 }
192 235 }
193 236
  237 + @ApiOperation(value = "Get Tenant Edges (getEdges)",
  238 + notes = "Returns a page of edges owned by tenant. " +
  239 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
194 240 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
195 241 @RequestMapping(value = "/edges", params = {"pageSize", "page"}, method = RequestMethod.GET)
196 242 @ResponseBody
197   - public PageData<Edge> getEdges(@RequestParam int pageSize,
  243 + public PageData<Edge> getEdges(@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
  244 + @RequestParam int pageSize,
  245 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
198 246 @RequestParam int page,
  247 + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION)
199 248 @RequestParam(required = false) String textSearch,
  249 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES)
200 250 @RequestParam(required = false) String sortProperty,
  251 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
201 252 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
202 253 try {
203 254 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
... ... @@ -208,10 +259,15 @@ public class EdgeController extends BaseController {
208 259 }
209 260 }
210 261
  262 + @ApiOperation(value = "Assign edge to customer (assignEdgeToCustomer)",
  263 + notes = "Creates assignment of the edge to customer. Customer will be able to query edge afterwards." + TENANT_AUTHORITY_PARAGRAPH,
  264 + produces = MediaType.APPLICATION_JSON_VALUE)
211 265 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
212 266 @RequestMapping(value = "/customer/{customerId}/edge/{edgeId}", method = RequestMethod.POST)
213 267 @ResponseBody
214   - public Edge assignEdgeToCustomer(@PathVariable("customerId") String strCustomerId,
  268 + public Edge assignEdgeToCustomer(@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
  269 + @PathVariable("customerId") String strCustomerId,
  270 + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
215 271 @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
216 272 checkParameter("customerId", strCustomerId);
217 273 checkParameter(EDGE_ID, strEdgeId);
... ... @@ -243,10 +299,14 @@ public class EdgeController extends BaseController {
243 299 }
244 300 }
245 301
  302 + @ApiOperation(value = "Unassign edge from customer (unassignEdgeFromCustomer)",
  303 + notes = "Clears assignment of the edge to customer. Customer will not be able to query edge afterwards." + TENANT_AUTHORITY_PARAGRAPH,
  304 + produces = MediaType.APPLICATION_JSON_VALUE)
246 305 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
247 306 @RequestMapping(value = "/customer/edge/{edgeId}", method = RequestMethod.DELETE)
248 307 @ResponseBody
249   - public Edge unassignEdgeFromCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
  308 + public Edge unassignEdgeFromCustomer(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  309 + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
250 310 checkParameter(EDGE_ID, strEdgeId);
251 311 try {
252 312 EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
... ... @@ -277,10 +337,16 @@ public class EdgeController extends BaseController {
277 337 }
278 338 }
279 339
  340 + @ApiOperation(value = "Make edge publicly available (assignEdgeToPublicCustomer)",
  341 + notes = "Edge will be available for non-authorized (not logged-in) users. " +
  342 + "This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
  343 + "However, users that are logged-in and belong to different tenant will not be able to access the edge." + TENANT_AUTHORITY_PARAGRAPH,
  344 + produces = MediaType.APPLICATION_JSON_VALUE)
280 345 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
281 346 @RequestMapping(value = "/customer/public/edge/{edgeId}", method = RequestMethod.POST)
282 347 @ResponseBody
283   - public Edge assignEdgeToPublicCustomer(@PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
  348 + public Edge assignEdgeToPublicCustomer(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  349 + @PathVariable(EDGE_ID) String strEdgeId) throws ThingsboardException {
284 350 checkParameter(EDGE_ID, strEdgeId);
285 351 try {
286 352 EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
... ... @@ -304,15 +370,24 @@ public class EdgeController extends BaseController {
304 370 }
305 371 }
306 372
  373 + @ApiOperation(value = "Get Tenant Edges (getTenantEdges)",
  374 + notes = "Returns a page of edges owned by tenant. " +
  375 + PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
307 376 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
308 377 @RequestMapping(value = "/tenant/edges", params = {"pageSize", "page"}, method = RequestMethod.GET)
309 378 @ResponseBody
310 379 public PageData<Edge> getTenantEdges(
  380 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
311 381 @RequestParam int pageSize,
  382 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
312 383 @RequestParam int page,
  384 + @ApiParam(value = EDGE_TYPE_DESCRIPTION)
313 385 @RequestParam(required = false) String type,
  386 + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION)
314 387 @RequestParam(required = false) String textSearch,
  388 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES)
315 389 @RequestParam(required = false) String sortProperty,
  390 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
316 391 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
317 392 try {
318 393 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -327,15 +402,25 @@ public class EdgeController extends BaseController {
327 402 }
328 403 }
329 404
  405 + @ApiOperation(value = "Get Tenant Edge Infos (getTenantEdgeInfos)",
  406 + notes = "Returns a page of edges info objects owned by tenant. " +
  407 + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH,
  408 + produces = MediaType.APPLICATION_JSON_VALUE)
330 409 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
331 410 @RequestMapping(value = "/tenant/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
332 411 @ResponseBody
333 412 public PageData<EdgeInfo> getTenantEdgeInfos(
  413 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
334 414 @RequestParam int pageSize,
  415 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
335 416 @RequestParam int page,
  417 + @ApiParam(value = EDGE_TYPE_DESCRIPTION)
336 418 @RequestParam(required = false) String type,
  419 + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION)
337 420 @RequestParam(required = false) String textSearch,
  421 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES)
338 422 @RequestParam(required = false) String sortProperty,
  423 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
339 424 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
340 425 try {
341 426 TenantId tenantId = getCurrentUser().getTenantId();
... ... @@ -350,10 +435,15 @@ public class EdgeController extends BaseController {
350 435 }
351 436 }
352 437
  438 + @ApiOperation(value = "Get Tenant Edge (getTenantEdge)",
  439 + notes = "Requested edge must be owned by tenant or customer that the user belongs to. " +
  440 + "Edge name is an unique property of edge. So it can be used to identify the edge." + TENANT_AUTHORITY_PARAGRAPH,
  441 + produces = MediaType.APPLICATION_JSON_VALUE)
353 442 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
354 443 @RequestMapping(value = "/tenant/edges", params = {"edgeName"}, method = RequestMethod.GET)
355 444 @ResponseBody
356   - public Edge getTenantEdge(@RequestParam String edgeName) throws ThingsboardException {
  445 + public Edge getTenantEdge(@ApiParam(value = "Unique name of the edge", required = true)
  446 + @RequestParam String edgeName) throws ThingsboardException {
357 447 try {
358 448 TenantId tenantId = getCurrentUser().getTenantId();
359 449 return checkNotNull(edgeService.findEdgeByTenantIdAndName(tenantId, edgeName));
... ... @@ -362,10 +452,16 @@ public class EdgeController extends BaseController {
362 452 }
363 453 }
364 454
  455 + @ApiOperation(value = "Set root rule chain for provided edge (setRootRuleChain)",
  456 + notes = "Change root rule chain of the edge to the new provided rule chain. \n" +
  457 + "This operation will send a notification to update root rule chain on remote edge service." + TENANT_AUTHORITY_PARAGRAPH,
  458 + produces = MediaType.APPLICATION_JSON_VALUE)
365 459 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
366 460 @RequestMapping(value = "/edge/{edgeId}/{ruleChainId}/root", method = RequestMethod.POST)
367 461 @ResponseBody
368   - public Edge setRootRuleChain(@PathVariable(EDGE_ID) String strEdgeId,
  462 + public Edge setRootRuleChain(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  463 + @PathVariable(EDGE_ID) String strEdgeId,
  464 + @ApiParam(value = RULE_CHAIN_ID_PARAM_DESCRIPTION, required = true)
369 465 @PathVariable("ruleChainId") String strRuleChainId) throws ThingsboardException {
370 466 checkParameter(EDGE_ID, strEdgeId);
371 467 checkParameter("ruleChainId", strRuleChainId);
... ... @@ -394,16 +490,26 @@ public class EdgeController extends BaseController {
394 490 }
395 491 }
396 492
  493 + @ApiOperation(value = "Get Customer Edges (getCustomerEdges)",
  494 + notes = "Returns a page of edges objects assigned to customer. " +
  495 + PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
397 496 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
398 497 @RequestMapping(value = "/customer/{customerId}/edges", params = {"pageSize", "page"}, method = RequestMethod.GET)
399 498 @ResponseBody
400 499 public PageData<Edge> getCustomerEdges(
  500 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
401 501 @PathVariable("customerId") String strCustomerId,
  502 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
402 503 @RequestParam int pageSize,
  504 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
403 505 @RequestParam int page,
  506 + @ApiParam(value = EDGE_TYPE_DESCRIPTION)
404 507 @RequestParam(required = false) String type,
  508 + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION)
405 509 @RequestParam(required = false) String textSearch,
  510 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES)
406 511 @RequestParam(required = false) String sortProperty,
  512 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
407 513 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
408 514 checkParameter("customerId", strCustomerId);
409 515 try {
... ... @@ -429,16 +535,26 @@ public class EdgeController extends BaseController {
429 535 }
430 536 }
431 537
  538 + @ApiOperation(value = "Get Customer Edge Infos (getCustomerEdgeInfos)",
  539 + notes = "Returns a page of edges info objects assigned to customer. " +
  540 + PAGE_DATA_PARAMETERS + EDGE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
432 541 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
433 542 @RequestMapping(value = "/customer/{customerId}/edgeInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
434 543 @ResponseBody
435 544 public PageData<EdgeInfo> getCustomerEdgeInfos(
  545 + @ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
436 546 @PathVariable("customerId") String strCustomerId,
  547 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
437 548 @RequestParam int pageSize,
  549 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
438 550 @RequestParam int page,
  551 + @ApiParam(value = EDGE_TYPE_DESCRIPTION)
439 552 @RequestParam(required = false) String type,
  553 + @ApiParam(value = EDGE_TEXT_SEARCH_DESCRIPTION)
440 554 @RequestParam(required = false) String textSearch,
  555 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES)
441 556 @RequestParam(required = false) String sortProperty,
  557 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
442 558 @RequestParam(required = false) String sortOrder) throws ThingsboardException {
443 559 checkParameter("customerId", strCustomerId);
444 560 try {
... ... @@ -464,10 +580,14 @@ public class EdgeController extends BaseController {
464 580 }
465 581 }
466 582
  583 + @ApiOperation(value = "Get Edges By Ids (getEdgesByIds)",
  584 + notes = "Requested edges must be owned by tenant or assigned to customer which user is performing the request." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  585 + produces = MediaType.APPLICATION_JSON_VALUE)
467 586 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
468 587 @RequestMapping(value = "/edges", params = {"edgeIds"}, method = RequestMethod.GET)
469 588 @ResponseBody
470 589 public List<Edge> getEdgesByIds(
  590 + @ApiParam(value = "A list of edges ids, separated by comma ','", required = true)
471 591 @RequestParam("edgeIds") String[] strEdgeIds) throws ThingsboardException {
472 592 checkArrayParameter("edgeIds", strEdgeIds);
473 593 try {
... ... @@ -496,6 +616,11 @@ public class EdgeController extends BaseController {
496 616 }
497 617 }
498 618
  619 + @ApiOperation(value = "Find related edges (findByQuery)",
  620 + notes = "Returns all edges that are related to the specific entity. " +
  621 + "The entity id, relation type, edge types, depth of the search, and other query parameters defined using complex 'EdgeSearchQuery' object. " +
  622 + "See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  623 + produces = MediaType.APPLICATION_JSON_VALUE)
499 624 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
500 625 @RequestMapping(value = "/edges", method = RequestMethod.POST)
501 626 @ResponseBody
... ... @@ -527,6 +652,10 @@ public class EdgeController extends BaseController {
527 652 }
528 653 }
529 654
  655 + @ApiOperation(value = "Get Edge Types (getEdgeTypes)",
  656 + 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."
  657 + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
  658 + produces = MediaType.APPLICATION_JSON_VALUE)
530 659 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
531 660 @RequestMapping(value = "/edge/types", method = RequestMethod.GET)
532 661 @ResponseBody
... ... @@ -541,9 +670,13 @@ public class EdgeController extends BaseController {
541 670 }
542 671 }
543 672
  673 + @ApiOperation(value = "Sync edge (syncEdge)",
  674 + notes = "Starts synchronization process between edge and cloud. \n" +
  675 + "All entities that are assigned to particular edge are going to be send to remote edge service." + TENANT_AUTHORITY_PARAGRAPH)
544 676 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
545 677 @RequestMapping(value = "/edge/sync/{edgeId}", method = RequestMethod.POST)
546   - public void syncEdge(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
  678 + public void syncEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  679 + @PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
547 680 checkParameter("edgeId", strEdgeId);
548 681 try {
549 682 if (isEdgesEnabled()) {
... ... @@ -560,10 +693,13 @@ public class EdgeController extends BaseController {
560 693 }
561 694 }
562 695
  696 + @ApiOperation(value = "Find missing rule chains (findMissingToRelatedRuleChains)",
  697 + 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)
563 698 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
564 699 @RequestMapping(value = "/edge/missingToRelatedRuleChains/{edgeId}", method = RequestMethod.GET)
565 700 @ResponseBody
566   - public String findMissingToRelatedRuleChains(@PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
  701 + public String findMissingToRelatedRuleChains(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
  702 + @PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
567 703 try {
568 704 EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
569 705 edgeId = checkNotNull(edgeId);
... ... @@ -575,9 +711,12 @@ public class EdgeController extends BaseController {
575 711 }
576 712 }
577 713
  714 + @ApiOperation(value = "Import the bulk of edges (processEdgesBulkImport)",
  715 + notes = "There's an ability to import the bulk of edges using the only .csv file." + TENANT_AUTHORITY_PARAGRAPH,
  716 + produces = MediaType.APPLICATION_JSON_VALUE)
578 717 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
579 718 @PostMapping("/edge/bulk_import")
580   - public BulkImportResult<Edge> processEdgeBulkImport(@RequestBody BulkImportRequest request) throws Exception {
  719 + public BulkImportResult<Edge> processEdgesBulkImport(@RequestBody BulkImportRequest request) throws Exception {
581 720 SecurityUser user = getCurrentUser();
582 721 RuleChain edgeTemplateRootRuleChain = ruleChainService.getEdgeTemplateRootRuleChain(user.getTenantId());
583 722 if (edgeTemplateRootRuleChain == null) {
... ... @@ -586,7 +725,7 @@ public class EdgeController extends BaseController {
586 725
587 726 return edgeBulkImportService.processBulkImport(request, user, importedAssetInfo -> {
588 727 try {
589   - onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated());
  728 + onEdgeCreatedOrUpdated(user.getTenantId(), importedAssetInfo.getEntity(), edgeTemplateRootRuleChain, importedAssetInfo.isUpdated(), user);
590 729 } catch (Exception e) {
591 730 throw new RuntimeException(e);
592 731 }
... ...
... ... @@ -15,8 +15,11 @@
15 15 */
16 16 package org.thingsboard.server.controller;
17 17
  18 +import io.swagger.annotations.ApiOperation;
  19 +import io.swagger.annotations.ApiParam;
18 20 import lombok.extern.slf4j.Slf4j;
19 21 import org.springframework.beans.factory.annotation.Autowired;
  22 +import org.springframework.http.MediaType;
20 23 import org.springframework.security.access.prepost.PreAuthorize;
21 24 import org.springframework.web.bind.annotation.PathVariable;
22 25 import org.springframework.web.bind.annotation.RequestMapping;
... ... @@ -34,6 +37,15 @@ import org.thingsboard.server.dao.edge.EdgeEventService;
34 37 import org.thingsboard.server.queue.util.TbCoreComponent;
35 38 import org.thingsboard.server.service.security.permission.Operation;
36 39
  40 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
  41 +import static org.thingsboard.server.controller.ControllerConstants.EDGE_SORT_PROPERTY_ALLOWABLE_VALUES;
  42 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
  43 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
  44 +import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
  45 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
  46 +import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
  47 +import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
  48 +
37 49 @Slf4j
38 50 @RestController
39 51 @TbCoreComponent
... ... @@ -45,17 +57,28 @@ public class EdgeEventController extends BaseController {
45 57
46 58 public static final String EDGE_ID = "edgeId";
47 59
  60 + @ApiOperation(value = "Get Edge Events (getEdgeEvents)",
  61 + notes = "Returns a page of edge events for the requested edge. " +
  62 + PAGE_DATA_PARAMETERS, produces = MediaType.APPLICATION_JSON_VALUE)
48 63 @PreAuthorize("hasAuthority('TENANT_ADMIN')")
49 64 @RequestMapping(value = "/edge/{edgeId}/events", method = RequestMethod.GET)
50 65 @ResponseBody
51 66 public PageData<EdgeEvent> getEdgeEvents(
  67 + @ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
52 68 @PathVariable(EDGE_ID) String strEdgeId,
  69 + @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
53 70 @RequestParam int pageSize,
  71 + @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
54 72 @RequestParam int page,
  73 + @ApiParam(value = "The case insensitive 'startsWith' filter based on the edge event type name.")
55 74 @RequestParam(required = false) String textSearch,
  75 + @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = EDGE_SORT_PROPERTY_ALLOWABLE_VALUES)
56 76 @RequestParam(required = false) String sortProperty,
  77 + @ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
57 78 @RequestParam(required = false) String sortOrder,
  79 + @ApiParam(value = "Timestamp. Edge events with creation time before it won't be queried")
58 80 @RequestParam(required = false) Long startTime,
  81 + @ApiParam(value = "Timestamp. Edge events with creation time after it won't be queried")
59 82 @RequestParam(required = false) Long endTime) throws ThingsboardException {
60 83 checkParameter(EDGE_ID, strEdgeId);
61 84 try {
... ...