Commit 787f9f942ed92787b6d6b94bcdcd461bd2527bf0

Authored by Volodymyr Babak
2 parents fe2becb7 c2bc2871

Merge remote-tracking branch 'upstream/master'

Showing 96 changed files with 3549 additions and 207 deletions

Too many changes to show.

To preserve performance only 96 of 118 files are displayed.

@@ -496,6 +496,16 @@ @@ -496,6 +496,16 @@
496 <artifactId>extension-mqtt</artifactId> 496 <artifactId>extension-mqtt</artifactId>
497 <classifier>extension</classifier> 497 <classifier>extension</classifier>
498 </artifactItem> 498 </artifactItem>
  499 + <artifactItem>
  500 + <groupId>org.thingsboard.extensions</groupId>
  501 + <artifactId>extension-sqs</artifactId>
  502 + <classifier>extension</classifier>
  503 + </artifactItem>
  504 + <artifactItem>
  505 + <groupId>org.thingsboard.extensions</groupId>
  506 + <artifactId>extension-sns</artifactId>
  507 + <classifier>extension</classifier>
  508 + </artifactItem>
499 </artifactItems> 509 </artifactItems>
500 </configuration> 510 </configuration>
501 </execution> 511 </execution>
@@ -18,7 +18,7 @@ @@ -18,7 +18,7 @@
18 "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", 18 "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('alarms-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
19 "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 \"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 },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", 19 "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 \"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 },\n \"required\": []\n },\n \"form\": [\n \"alarmsTitle\",\n \"enableSelection\",\n \"enableSearch\",\n \"displayDetails\",\n \"allowAcknowledgment\",\n \"allowClear\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
20 "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)\",\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, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", 20 "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)\",\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, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
21 - "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\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"18px\",\"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}" 21 + "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\"},\"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}"
22 } 22 }
23 } 23 }
24 ] 24 ]
@@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
34 "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", 34 "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('entities-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
35 "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 \"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 \"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 },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}", 35 "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 \"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 \"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 },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
36 "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)\",\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, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", 36 "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)\",\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, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
37 - "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\":\"18px\",\"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;\"}]}]}" 37 + "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;\"}]}]}"
38 } 38 }
39 }, 39 },
40 { 40 {
  1 +{
  2 + "widgetsBundle": {
  3 + "alias": "gateway_widgets",
  4 + "title": "Gateway widgets",
  5 + "image": null
  6 + },
  7 + "widgetTypes": [
  8 + {
  9 + "alias": "extension_configuration_widget",
  10 + "name": "Extensions table",
  11 + "descriptor": {
  12 + "type": "latest",
  13 + "sizeX": 9,
  14 + "sizeY": 6.5,
  15 + "resources": [],
  16 + "templateHtml": "<tb-extensions-table-widget \n ctx=\"ctx\">\n</tb-extensions-table-widget>",
  17 + "templateCss": "#container {\n overflow: auto;\n}",
  18 + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1\n };\n}\n\nself.onDestroy = function() {\n}\n",
  19 + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"ExtensionTableSettings\",\n \"properties\": {\n \"extensionsTitle\": {\n \"title\": \"Extension table title\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"extensionsTitle\"\n ]\n}",
  20 + "dataKeySettingsSchema": "{}\n",
  21 + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Extensions table\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
  22 + }
  23 + }
  24 + ]
  25 +}
@@ -170,7 +170,7 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> { @@ -170,7 +170,7 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
170 Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd); 170 Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
171 if (ruleToPluginMsgOptional.isPresent()) { 171 if (ruleToPluginMsgOptional.isPresent()) {
172 RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get(); 172 RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
173 - logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg); 173 + logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg);
174 context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self()); 174 context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
175 if (action.isOneWayAction()) { 175 if (action.isOneWayAction()) {
176 pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS); 176 pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);
@@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationManager; @@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationManager;
28 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 28 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
29 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 29 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
30 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 30 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  31 +import org.springframework.security.config.annotation.web.builders.WebSecurity;
31 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 32 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
32 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 33 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
33 import org.springframework.security.config.http.SessionCreationPolicy; 34 import org.springframework.security.config.http.SessionCreationPolicy;
@@ -148,6 +149,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @@ -148,6 +149,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
148 } 149 }
149 150
150 @Override 151 @Override
  152 + public void configure(WebSecurity web) throws Exception {
  153 + web.ignoring().antMatchers("/static/**");
  154 + }
  155 +
  156 + @Override
151 protected void configure(HttpSecurity http) throws Exception { 157 protected void configure(HttpSecurity http) throws Exception {
152 http.headers().cacheControl().and().frameOptions().disable() 158 http.headers().cacheControl().and().frameOptions().disable()
153 .and() 159 .and()
@@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; @@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
19 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 import com.fasterxml.jackson.databind.node.ObjectNode; 20 import com.fasterxml.jackson.databind.node.ObjectNode;
21 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
22 -import org.slf4j.Logger;  
23 -import org.slf4j.LoggerFactory;  
24 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
25 import org.springframework.http.HttpHeaders; 23 import org.springframework.http.HttpHeaders;
26 import org.springframework.http.HttpStatus; 24 import org.springframework.http.HttpStatus;
@@ -30,7 +28,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -30,7 +28,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
30 import org.springframework.web.bind.annotation.*; 28 import org.springframework.web.bind.annotation.*;
31 import org.thingsboard.server.common.data.User; 29 import org.thingsboard.server.common.data.User;
32 import org.thingsboard.server.common.data.security.UserCredentials; 30 import org.thingsboard.server.common.data.security.UserCredentials;
33 -import org.thingsboard.server.dao.user.UserService;  
34 import org.thingsboard.server.exception.ThingsboardErrorCode; 31 import org.thingsboard.server.exception.ThingsboardErrorCode;
35 import org.thingsboard.server.exception.ThingsboardException; 32 import org.thingsboard.server.exception.ThingsboardException;
36 import org.thingsboard.server.service.mail.MailService; 33 import org.thingsboard.server.service.mail.MailService;
@@ -78,9 +75,10 @@ public class AuthController extends BaseController { @@ -78,9 +75,10 @@ public class AuthController extends BaseController {
78 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST) 75 @RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
79 @ResponseStatus(value = HttpStatus.OK) 76 @ResponseStatus(value = HttpStatus.OK)
80 public void changePassword ( 77 public void changePassword (
81 - @RequestParam(value = "currentPassword") String currentPassword,  
82 - @RequestParam(value = "newPassword") String newPassword) throws ThingsboardException { 78 + @RequestBody JsonNode changePasswordRequest) throws ThingsboardException {
83 try { 79 try {
  80 + String currentPassword = changePasswordRequest.get("currentPassword").asText();
  81 + String newPassword = changePasswordRequest.get("newPassword").asText();
84 SecurityUser securityUser = getCurrentUser(); 82 SecurityUser securityUser = getCurrentUser();
85 UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId()); 83 UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId());
86 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) { 84 if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
@@ -118,9 +116,10 @@ public class AuthController extends BaseController { @@ -118,9 +116,10 @@ public class AuthController extends BaseController {
118 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST) 116 @RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
119 @ResponseStatus(value = HttpStatus.OK) 117 @ResponseStatus(value = HttpStatus.OK)
120 public void requestResetPasswordByEmail ( 118 public void requestResetPasswordByEmail (
121 - @RequestParam(value = "email") String email, 119 + @RequestBody JsonNode resetPasswordByEmailRequest,
122 HttpServletRequest request) throws ThingsboardException { 120 HttpServletRequest request) throws ThingsboardException {
123 try { 121 try {
  122 + String email = resetPasswordByEmailRequest.get("email").asText();
124 UserCredentials userCredentials = userService.requestPasswordReset(email); 123 UserCredentials userCredentials = userService.requestPasswordReset(email);
125 String baseUrl = constructBaseUrl(request); 124 String baseUrl = constructBaseUrl(request);
126 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl, 125 String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
@@ -158,10 +157,11 @@ public class AuthController extends BaseController { @@ -158,10 +157,11 @@ public class AuthController extends BaseController {
158 @ResponseStatus(value = HttpStatus.OK) 157 @ResponseStatus(value = HttpStatus.OK)
159 @ResponseBody 158 @ResponseBody
160 public JsonNode activateUser( 159 public JsonNode activateUser(
161 - @RequestParam(value = "activateToken") String activateToken,  
162 - @RequestParam(value = "password") String password, 160 + @RequestBody JsonNode activateRequest,
163 HttpServletRequest request) throws ThingsboardException { 161 HttpServletRequest request) throws ThingsboardException {
164 try { 162 try {
  163 + String activateToken = activateRequest.get("activateToken").asText();
  164 + String password = activateRequest.get("password").asText();
165 String encodedPassword = passwordEncoder.encode(password); 165 String encodedPassword = passwordEncoder.encode(password);
166 UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword); 166 UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
167 User user = userService.findUserById(credentials.getUserId()); 167 User user = userService.findUserById(credentials.getUserId());
@@ -194,10 +194,11 @@ public class AuthController extends BaseController { @@ -194,10 +194,11 @@ public class AuthController extends BaseController {
194 @ResponseStatus(value = HttpStatus.OK) 194 @ResponseStatus(value = HttpStatus.OK)
195 @ResponseBody 195 @ResponseBody
196 public JsonNode resetPassword( 196 public JsonNode resetPassword(
197 - @RequestParam(value = "resetToken") String resetToken,  
198 - @RequestParam(value = "password") String password, 197 + @RequestBody JsonNode resetPasswordRequest,
199 HttpServletRequest request) throws ThingsboardException { 198 HttpServletRequest request) throws ThingsboardException {
200 try { 199 try {
  200 + String resetToken = resetPasswordRequest.get("resetToken").asText();
  201 + String password = resetPasswordRequest.get("password").asText();
201 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken); 202 UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
202 if (userCredentials != null) { 203 if (userCredentials != null) {
203 String encodedPassword = passwordEncoder.encode(password); 204 String encodedPassword = passwordEncoder.encode(password);
@@ -221,7 +221,10 @@ public abstract class AbstractControllerTest { @@ -221,7 +221,10 @@ public abstract class AbstractControllerTest {
221 doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) 221 doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
222 .andExpect(status().isSeeOther()) 222 .andExpect(status().isSeeOther())
223 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); 223 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
224 - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class); 224 + JsonNode activateRequest = new ObjectMapper().createObjectNode()
  225 + .put("activateToken", TestMailService.currentActivateToken)
  226 + .put("password", password);
  227 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
225 validateAndSetJwtToken(tokenInfo, user.getEmail()); 228 validateAndSetJwtToken(tokenInfo, user.getEmail());
226 return savedUser; 229 return savedUser;
227 } 230 }
@@ -17,6 +17,7 @@ package org.thingsboard.server.controller; @@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
17 17
18 import com.fasterxml.jackson.core.type.TypeReference; 18 import com.fasterxml.jackson.core.type.TypeReference;
19 import com.fasterxml.jackson.databind.JsonNode; 19 import com.fasterxml.jackson.databind.JsonNode;
  20 +import com.fasterxml.jackson.databind.ObjectMapper;
20 import org.apache.commons.lang3.RandomStringUtils; 21 import org.apache.commons.lang3.RandomStringUtils;
21 import org.junit.Assert; 22 import org.junit.Assert;
22 import org.junit.Test; 23 import org.junit.Test;
@@ -73,7 +74,11 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { @@ -73,7 +74,11 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
73 .andExpect(status().isSeeOther()) 74 .andExpect(status().isSeeOther())
74 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); 75 .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
75 76
76 - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class); 77 + JsonNode activateRequest = new ObjectMapper().createObjectNode()
  78 + .put("activateToken", TestMailService.currentActivateToken)
  79 + .put("password", "testPassword");
  80 +
  81 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
77 validateAndSetJwtToken(tokenInfo, email); 82 validateAndSetJwtToken(tokenInfo, email);
78 83
79 doGet("/api/auth/user") 84 doGet("/api/auth/user")
@@ -117,13 +122,21 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { @@ -117,13 +122,21 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
117 122
118 User savedUser = createUserAndLogin(user, "testPassword1"); 123 User savedUser = createUserAndLogin(user, "testPassword1");
119 logout(); 124 logout();
120 - doPost("/api/noauth/resetPasswordByEmail", "email", email) 125 +
  126 + JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
  127 + .put("email", email);
  128 +
  129 + doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
121 .andExpect(status().isOk()); 130 .andExpect(status().isOk());
122 doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken) 131 doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
123 .andExpect(status().isSeeOther()) 132 .andExpect(status().isSeeOther())
124 .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken)); 133 .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
125 -  
126 - JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class); 134 +
  135 + JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
  136 + .put("resetToken", TestMailService.currentResetPasswordToken)
  137 + .put("password", "testPassword2");
  138 +
  139 + JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isOk()), JsonNode.class);
127 validateAndSetJwtToken(tokenInfo, email); 140 validateAndSetJwtToken(tokenInfo, email);
128 141
129 doGet("/api/auth/user") 142 doGet("/api/auth/user")
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server Common Data</name> 30 <name>Thingsboard Server Common Data</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server Common Messages</name> 30 <name>Thingsboard Server Common Messages</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>pom</packaging> 28 <packaging>pom</packaging>
29 29
30 <name>Thingsboard Server Commons</name> 30 <name>Thingsboard Server Commons</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server Common Transport components</name> 30 <name>Thingsboard Server Common Transport components</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server DAO Layer</name> 30 <name>Thingsboard Server DAO Layer</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -384,7 +384,7 @@ public class BaseRelationService implements RelationService { @@ -384,7 +384,7 @@ public class BaseRelationService implements RelationService {
384 Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get()); 384 Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
385 Set<EntityId> childrenIds = new HashSet<>(); 385 Set<EntityId> childrenIds = new HashSet<>();
386 for (EntityRelation childRelation : children) { 386 for (EntityRelation childRelation : children) {
387 - log.info("Found Relation: {}", childRelation); 387 + log.trace("Found Relation: {}", childRelation);
388 EntityId childId; 388 EntityId childId;
389 if (direction == EntitySearchDirection.FROM) { 389 if (direction == EntitySearchDirection.FROM) {
390 childId = childRelation.getTo(); 390 childId = childRelation.getTo();
@@ -392,9 +392,9 @@ public class BaseRelationService implements RelationService { @@ -392,9 +392,9 @@ public class BaseRelationService implements RelationService {
392 childId = childRelation.getFrom(); 392 childId = childRelation.getFrom();
393 } 393 }
394 if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) { 394 if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
395 - log.info("Adding Relation: {}", childId); 395 + log.trace("Adding Relation: {}", childId);
396 if (childrenIds.add(childId)) { 396 if (childrenIds.add(childId)) {
397 - log.info("Added Relation: {}", childId); 397 + log.trace("Added Relation: {}", childId);
398 } 398 }
399 } 399 }
400 } 400 }
@@ -22,6 +22,7 @@ import javax.annotation.PreDestroy; @@ -22,6 +22,7 @@ import javax.annotation.PreDestroy;
22 import java.util.concurrent.Executors; 22 import java.util.concurrent.Executors;
23 23
24 public abstract class JpaAbstractDaoListeningExecutorService { 24 public abstract class JpaAbstractDaoListeningExecutorService {
  25 +
25 protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); 26 protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
26 27
27 @PreDestroy 28 @PreDestroy
@@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries; @@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
17 17
18 import com.google.common.base.Function; 18 import com.google.common.base.Function;
19 import com.google.common.collect.Lists; 19 import com.google.common.collect.Lists;
20 -import com.google.common.util.concurrent.Futures;  
21 -import com.google.common.util.concurrent.ListenableFuture;  
22 -import com.google.common.util.concurrent.SettableFuture; 20 +import com.google.common.util.concurrent.*;
23 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
24 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
25 import org.springframework.data.domain.PageRequest; 23 import org.springframework.data.domain.PageRequest;
@@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao; @@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
36 import org.thingsboard.server.dao.util.SqlDao; 34 import org.thingsboard.server.dao.util.SqlDao;
37 35
38 import javax.annotation.Nullable; 36 import javax.annotation.Nullable;
  37 +import javax.annotation.PreDestroy;
39 import java.util.ArrayList; 38 import java.util.ArrayList;
40 import java.util.List; 39 import java.util.List;
41 import java.util.Optional; 40 import java.util.Optional;
42 import java.util.concurrent.CompletableFuture; 41 import java.util.concurrent.CompletableFuture;
  42 +import java.util.concurrent.Executors;
43 import java.util.stream.Collectors; 43 import java.util.stream.Collectors;
44 44
45 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; 45 import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; @@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
50 @SqlDao 50 @SqlDao
51 public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao { 51 public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
52 52
  53 + private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
  54 +
53 @Autowired 55 @Autowired
54 private TsKvRepository tsKvRepository; 56 private TsKvRepository tsKvRepository;
55 57
@@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp @@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
232 entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); 234 entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
233 entity.setLongValue(tsKvEntry.getLongValue().orElse(null)); 235 entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
234 entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); 236 entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
235 - return service.submit(() -> { 237 + log.trace("Saving entity: " + entity);
  238 + return insertService.submit(() -> {
236 tsKvRepository.save(entity); 239 tsKvRepository.save(entity);
237 return null; 240 return null;
238 }); 241 });
@@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp @@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
240 243
241 @Override 244 @Override
242 public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) { 245 public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
243 - return service.submit(() -> null); 246 + return insertService.submit(() -> null);
244 } 247 }
245 248
246 @Override 249 @Override
@@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp @@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
254 latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null)); 257 latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
255 latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null)); 258 latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
256 latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null)); 259 latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
257 - return service.submit(() -> { 260 + return insertService.submit(() -> {
258 tsKvLatestRepository.save(latestEntity); 261 tsKvLatestRepository.save(latestEntity);
259 return null; 262 return null;
260 }); 263 });
261 } 264 }
262 265
  266 + @PreDestroy
  267 + void onDestroy() {
  268 + insertService.shutdown();
  269 + }
  270 +
263 } 271 }
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server Extensions API</name> 30 <name>Thingsboard Server Extensions API</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server Core Extensions</name> 30 <name>Thingsboard Server Core Extensions</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -128,12 +128,16 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler { @@ -128,12 +128,16 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
128 Optional<Long> interval = request.getLongParamValue("interval"); 128 Optional<Long> interval = request.getLongParamValue("interval");
129 Optional<Integer> limit = request.getIntParamValue("limit"); 129 Optional<Integer> limit = request.getIntParamValue("limit");
130 130
  131 + // If some of these params are specified, they all must be
131 if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) { 132 if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
132 - if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) { 133 + if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent() || interval.get() < 0) {
133 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); 134 msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
134 return; 135 return;
135 } 136 }
136 - Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name())); 137 +
  138 + // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
  139 + Aggregation agg = (interval.isPresent() && interval.get() == 0) ? Aggregation.valueOf(Aggregation.NONE.name()) :
  140 + Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
137 141
138 List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)) 142 List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
139 .collect(Collectors.toList()); 143 .collect(Collectors.toList());
@@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
30 <packaging>jar</packaging> 30 <packaging>jar</packaging>
31 31
32 <name>Thingsboard Server Kafka Extension</name> 32 <name>Thingsboard Server Kafka Extension</name>
33 - <url>http://thingsboard.org</url> 33 + <url>https://thingsboard.io</url>
34 34
35 <properties> 35 <properties>
36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
30 <packaging>jar</packaging> 30 <packaging>jar</packaging>
31 31
32 <name>Thingsboard Server MQTT Extension</name> 32 <name>Thingsboard Server MQTT Extension</name>
33 - <url>http://thingsboard.org</url> 33 + <url>https://thingsboard.io</url>
34 34
35 <properties> 35 <properties>
36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server RabbitMQ Extension</name> 30 <name>Thingsboard Server RabbitMQ Extension</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
30 <packaging>jar</packaging> 30 <packaging>jar</packaging>
31 31
32 <name>Thingsboard Server REST API Call Extension</name> 32 <name>Thingsboard Server REST API Call Extension</name>
33 - <url>http://thingsboard.org</url> 33 + <url>https://thingsboard.io</url>
34 34
35 <properties> 35 <properties>
36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 36 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!--
  3 +
  4 + Copyright © 2016-2017 The Thingsboard Authors
  5 +
  6 + Licensed under the Apache License, Version 2.0 (the "License");
  7 + you may not use this file except in compliance with the License.
  8 + You may obtain a copy of the License at
  9 +
  10 + http://www.apache.org/licenses/LICENSE-2.0
  11 +
  12 + Unless required by applicable law or agreed to in writing, software
  13 + distributed under the License is distributed on an "AS IS" BASIS,
  14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 + See the License for the specific language governing permissions and
  16 + limitations under the License.
  17 +
  18 +-->
  19 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  20 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  21 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  22 + <parent>
  23 + <artifactId>extensions</artifactId>
  24 + <groupId>org.thingsboard</groupId>
  25 + <version>1.4.0-SNAPSHOT</version>
  26 + </parent>
  27 + <modelVersion>4.0.0</modelVersion>
  28 + <groupId>org.thingsboard.extensions</groupId>
  29 + <artifactId>extension-sns</artifactId>
  30 + <packaging>jar</packaging>
  31 +
  32 + <name>Thingsboard Server SNS Extension</name>
  33 + <url>https://thingsboard.io</url>
  34 +
  35 + <properties>
  36 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  37 + <main.dir>${basedir}/../..</main.dir>
  38 + <aws.sdk.version>1.11.229</aws.sdk.version>
  39 + </properties>
  40 +
  41 + <dependencies>
  42 + <dependency>
  43 + <groupId>org.thingsboard</groupId>
  44 + <artifactId>extensions-api</artifactId>
  45 + <scope>provided</scope>
  46 + </dependency>
  47 + <dependency>
  48 + <groupId>org.thingsboard</groupId>
  49 + <artifactId>extensions-core</artifactId>
  50 + <scope>provided</scope>
  51 + </dependency>
  52 + <dependency>
  53 + <groupId>com.amazonaws</groupId>
  54 + <artifactId>aws-java-sdk-sns</artifactId>
  55 + <version>${aws.sdk.version}</version>
  56 + </dependency>
  57 + </dependencies>
  58 +
  59 + <build>
  60 + <plugins>
  61 + <plugin>
  62 + <artifactId>maven-assembly-plugin</artifactId>
  63 + <configuration>
  64 + <descriptors>
  65 + <descriptor>src/assembly/extension.xml</descriptor>
  66 + </descriptors>
  67 + </configuration>
  68 + <executions>
  69 + <execution>
  70 + <id>make-assembly</id>
  71 + <phase>package</phase>
  72 + <goals>
  73 + <goal>single</goal>
  74 + </goals>
  75 + </execution>
  76 + </executions>
  77 + </plugin>
  78 + </plugins>
  79 + </build>
  80 +
  81 +</project>
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
  19 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  20 + xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
  21 + <id>extension</id>
  22 + <formats>
  23 + <format>jar</format>
  24 + </formats>
  25 + <includeBaseDirectory>false</includeBaseDirectory>
  26 + <dependencySets>
  27 + <dependencySet>
  28 + <outputDirectory>/</outputDirectory>
  29 + <useProjectArtifact>true</useProjectArtifact>
  30 + <unpack>true</unpack>
  31 + <scope>runtime</scope>
  32 + <excludes>
  33 +
  34 + </excludes>
  35 + </dependencySet>
  36 + </dependencySets>
  37 +</assembly>
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.action;
  17 +
  18 +import org.thingsboard.server.common.data.id.CustomerId;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  22 +
  23 +/**
  24 + * Created by Valerii Sosliuk on 11/15/2017.
  25 + */
  26 +public class SnsTopicActionMsg extends AbstractRuleToPluginMsg<SnsTopicActionPayload> {
  27 +
  28 + public SnsTopicActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SnsTopicActionPayload payload) {
  29 + super(tenantId, customerId, deviceId, payload);
  30 + }
  31 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.action;
  17 +
  18 +import lombok.Builder;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +
  22 +import java.io.Serializable;
  23 +
  24 +/**
  25 + * Created by Valerii Sosliuk on 11/15/2017.
  26 + */
  27 +@Data
  28 +@Builder
  29 +public class SnsTopicActionPayload implements Serializable {
  30 +
  31 + private final String topicArn;
  32 + private final String msgBody;
  33 +
  34 + private final Integer requestId;
  35 + private final MsgType msgType;
  36 + private final boolean sync;
  37 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.action;
  17 +
  18 +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  20 +import org.thingsboard.server.extensions.api.component.Action;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  22 +import org.thingsboard.server.extensions.api.rules.RuleContext;
  23 +import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
  24 +
  25 +import java.util.Optional;
  26 +
  27 +/**
  28 + * Created by Valerii Sosliuk on 11/15/2017.
  29 + */
  30 +@Action(name = "SNS Topic Action", descriptor = "SnsTopicActionDescriptor.json", configuration = SnsTopicPluginActionConfiguration.class)
  31 +public class SnsTopicPluginAction extends AbstractTemplatePluginAction<SnsTopicPluginActionConfiguration> {
  32 +
  33 + @Override
  34 + protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
  35 + SnsTopicActionPayload.SnsTopicActionPayloadBuilder builder = SnsTopicActionPayload.builder();
  36 + builder.msgType(payload.getMsgType());
  37 + builder.requestId(payload.getRequestId());
  38 + builder.topicArn(configuration.getTopicArn());
  39 + builder.msgBody(getMsgBody(ctx, msg));
  40 + return Optional.of(new SnsTopicActionMsg(msg.getTenantId(),
  41 + msg.getCustomerId(),
  42 + msg.getDeviceId(),
  43 + builder.build()));
  44 + }
  45 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.action;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
  20 +
  21 +/**
  22 + * Created by Valerii Sosliuk on 11/15/2017.
  23 + */
  24 +@Data
  25 +public class SnsTopicPluginActionConfiguration implements TemplateActionConfiguration {
  26 +
  27 + private String topicArn;
  28 + private String template;
  29 + private boolean sync;
  30 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.plugin;
  17 +
  18 +import com.amazonaws.services.sns.AmazonSNS;
  19 +import com.amazonaws.services.sns.model.PublishRequest;
  20 +import com.amazonaws.services.sns.model.PublishResult;
  21 +import com.amazonaws.services.sqs.AmazonSQS;
  22 +import com.amazonaws.services.sqs.model.SendMessageRequest;
  23 +import lombok.RequiredArgsConstructor;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +import org.thingsboard.server.common.data.id.RuleId;
  26 +import org.thingsboard.server.common.data.id.TenantId;
  27 +import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
  28 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  29 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  30 +import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
  31 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  32 +import org.thingsboard.server.extensions.api.rules.RuleException;
  33 +import org.thingsboard.server.extensions.sns.action.SnsTopicActionMsg;
  34 +import org.thingsboard.server.extensions.sns.action.SnsTopicActionPayload;
  35 +
  36 +/**
  37 + * Created by Valerii Sosliuk on 11/6/2017.
  38 + */
  39 +@RequiredArgsConstructor
  40 +@Slf4j
  41 +public class SnsMessageHandler implements RuleMsgHandler {
  42 +
  43 + private final AmazonSNS sns;
  44 +
  45 + @Override
  46 + public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
  47 + if (msg instanceof SnsTopicActionMsg) {
  48 + SnsTopicActionPayload payload = ((SnsTopicActionMsg) msg).getPayload();
  49 + PublishRequest publishRequest = new PublishRequest()
  50 + .withTopicArn(payload.getTopicArn())
  51 + .withMessage(payload.getMsgBody());
  52 + sns.publish(publishRequest);
  53 + if (payload.isSync()) {
  54 + ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
  55 + BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
  56 + }
  57 + return;
  58 + }
  59 + throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
  60 +
  61 + }
  62 +
  63 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.plugin;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.services.sns.AmazonSNS;
  22 +import com.amazonaws.services.sns.AmazonSNSClient;
  23 +import org.thingsboard.server.extensions.api.component.Plugin;
  24 +import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
  25 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  26 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  27 +import org.thingsboard.server.extensions.sns.action.SnsTopicPluginAction;
  28 +
  29 +/**
  30 + * Created by Valerii Sosliuk on 11/15/2017.
  31 + */
  32 +@Plugin(name = "SNS Plugin", actions = {SnsTopicPluginAction.class},
  33 + descriptor = "SnsPluginDescriptor.json", configuration = SnsPluginConfiguration.class)
  34 +public class SnsPlugin extends AbstractPlugin<SnsPluginConfiguration> {
  35 +
  36 + private SnsMessageHandler snsMessageHandler;
  37 + private SnsPluginConfiguration configuration;
  38 +
  39 + @Override
  40 + public void init(SnsPluginConfiguration configuration) {
  41 + this.configuration = configuration;
  42 + init();
  43 + }
  44 +
  45 + private void init() {
  46 + AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
  47 + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
  48 + AmazonSNS sns = AmazonSNSClient.builder()
  49 + .withCredentials(credProvider)
  50 + .withRegion(configuration.getRegion())
  51 + .build();
  52 + this.snsMessageHandler = new SnsMessageHandler(sns);
  53 +
  54 + }
  55 +
  56 + private void destroy() {
  57 + this.snsMessageHandler = null;
  58 + }
  59 +
  60 + @Override
  61 + protected RuleMsgHandler getRuleMsgHandler() {
  62 + return snsMessageHandler;
  63 + }
  64 +
  65 + @Override
  66 + public void resume(PluginContext ctx) {
  67 + init();
  68 + }
  69 +
  70 + @Override
  71 + public void suspend(PluginContext ctx) {
  72 + destroy();
  73 + }
  74 +
  75 + @Override
  76 + public void stop(PluginContext ctx) {
  77 + destroy();
  78 + }
  79 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sns.plugin;
  17 +
  18 +import lombok.Data;
  19 +
  20 +/**
  21 + * Created by Valerii Sosliuk on 11/5/2017.
  22 + */
  23 +@Data
  24 +public class SnsPluginConfiguration {
  25 +
  26 + private String accessKeyId;
  27 + private String secretAccessKey;
  28 + private String region;
  29 +
  30 +}
  1 +{
  2 + "schema": {
  3 + "title": "SNS Plugin Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "accessKeyId": {
  7 + "title": "Access Key ID",
  8 + "type": "string"
  9 + },
  10 + "secretAccessKey": {
  11 + "title": "Secret Access Key",
  12 + "type": "string"
  13 + },
  14 + "region": {
  15 + "title": "Region",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "accessKeyId",
  21 + "secretAccessKey",
  22 + "region"
  23 + ]
  24 + },
  25 + "form": [
  26 + "accessKeyId",
  27 + "secretAccessKey",
  28 + "region"
  29 + ]
  30 +}
  1 +{
  2 + "schema": {
  3 + "title": "SNS Topic Action Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "sync": {
  7 + "title": "Requires delivery confirmation",
  8 + "type": "boolean"
  9 + },
  10 + "topicArn": {
  11 + "title": "Topic ARN",
  12 + "type": "string"
  13 + },
  14 + "template": {
  15 + "title": "Body Template",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "sync",
  21 + "topicArn",
  22 + "template"
  23 + ]
  24 + },
  25 + "form": [
  26 + "sync",
  27 + "topicArn",
  28 + {
  29 + "key": "template",
  30 + "type": "textarea",
  31 + "rows": 5
  32 + }
  33 + ]
  34 +}
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<!--
  3 +
  4 + Copyright © 2016-2017 The Thingsboard Authors
  5 +
  6 + Licensed under the Apache License, Version 2.0 (the "License");
  7 + you may not use this file except in compliance with the License.
  8 + You may obtain a copy of the License at
  9 +
  10 + http://www.apache.org/licenses/LICENSE-2.0
  11 +
  12 + Unless required by applicable law or agreed to in writing, software
  13 + distributed under the License is distributed on an "AS IS" BASIS,
  14 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15 + See the License for the specific language governing permissions and
  16 + limitations under the License.
  17 +
  18 +-->
  19 +<project xmlns="http://maven.apache.org/POM/4.0.0"
  20 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  21 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  22 + <parent>
  23 + <artifactId>extensions</artifactId>
  24 + <groupId>org.thingsboard</groupId>
  25 + <version>1.4.0-SNAPSHOT</version>
  26 + </parent>
  27 + <modelVersion>4.0.0</modelVersion>
  28 + <groupId>org.thingsboard.extensions</groupId>
  29 + <artifactId>extension-sqs</artifactId>
  30 + <packaging>jar</packaging>
  31 +
  32 + <name>Thingsboard Server SQS Extension</name>
  33 + <url>https://thingsboard.io</url>
  34 +
  35 + <properties>
  36 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  37 + <main.dir>${basedir}/../..</main.dir>
  38 + <aws.sdk.version>1.11.229</aws.sdk.version>
  39 + </properties>
  40 +
  41 + <dependencies>
  42 + <dependency>
  43 + <groupId>org.thingsboard</groupId>
  44 + <artifactId>extensions-api</artifactId>
  45 + <scope>provided</scope>
  46 + </dependency>
  47 + <dependency>
  48 + <groupId>org.thingsboard</groupId>
  49 + <artifactId>extensions-core</artifactId>
  50 + <scope>provided</scope>
  51 + </dependency>
  52 + <dependency>
  53 + <groupId>com.amazonaws</groupId>
  54 + <artifactId>aws-java-sdk-sqs</artifactId>
  55 + <version>${aws.sdk.version}</version>
  56 + </dependency>
  57 + </dependencies>
  58 +
  59 + <build>
  60 + <plugins>
  61 + <plugin>
  62 + <artifactId>maven-assembly-plugin</artifactId>
  63 + <configuration>
  64 + <descriptors>
  65 + <descriptor>src/assembly/extension.xml</descriptor>
  66 + </descriptors>
  67 + </configuration>
  68 + <executions>
  69 + <execution>
  70 + <id>make-assembly</id>
  71 + <phase>package</phase>
  72 + <goals>
  73 + <goal>single</goal>
  74 + </goals>
  75 + </execution>
  76 + </executions>
  77 + </plugin>
  78 + </plugins>
  79 + </build>
  80 +
  81 +</project>
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
  19 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  20 + xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
  21 + <id>extension</id>
  22 + <formats>
  23 + <format>jar</format>
  24 + </formats>
  25 + <includeBaseDirectory>false</includeBaseDirectory>
  26 + <dependencySets>
  27 + <dependencySet>
  28 + <outputDirectory>/</outputDirectory>
  29 + <useProjectArtifact>true</useProjectArtifact>
  30 + <unpack>true</unpack>
  31 + <scope>runtime</scope>
  32 + <excludes>
  33 +
  34 + </excludes>
  35 + </dependencySet>
  36 + </dependencySets>
  37 +</assembly>
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.fifo;
  17 +
  18 +import org.thingsboard.server.common.data.id.CustomerId;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  22 +
  23 +/**
  24 + * Created by Valerii Sosliuk on 11/10/2017.
  25 + */
  26 +public class SqsFifoQueueActionMsg extends AbstractRuleToPluginMsg<SqsFifoQueueActionPayload> {
  27 +
  28 + public SqsFifoQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsFifoQueueActionPayload payload) {
  29 + super(tenantId, customerId, deviceId, payload);
  30 + }
  31 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.fifo;
  17 +
  18 +import lombok.Builder;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +
  22 +import java.io.Serializable;
  23 +
  24 +/**
  25 + * Created by Valerii Sosliuk on 11/10/2017.
  26 + */
  27 +@Data
  28 +@Builder
  29 +public class SqsFifoQueueActionPayload implements Serializable {
  30 +
  31 + private final String queue;
  32 + private final String msgBody;
  33 + private final String deviceId;
  34 +
  35 + private final Integer requestId;
  36 + private final MsgType msgType;
  37 + private final boolean sync;
  38 +
  39 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.fifo;
  17 +
  18 +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  20 +import org.thingsboard.server.extensions.api.component.Action;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  22 +import org.thingsboard.server.extensions.api.rules.RuleContext;
  23 +import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
  24 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
  25 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
  26 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginActionConfiguration;
  27 +
  28 +import java.util.Optional;
  29 +
  30 +/**
  31 + * Created by Valerii Sosliuk on 11/5/2017.
  32 + */
  33 +@Action(name = "SQS Fifo Queue Action", descriptor = "SqsFifoQueueActionDescriptor.json", configuration = SqsFifoQueuePluginActionConfiguration.class)
  34 +public class SqsFifoQueuePluginAction extends AbstractTemplatePluginAction<SqsFifoQueuePluginActionConfiguration> {
  35 +
  36 + @Override
  37 + protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
  38 + SqsFifoQueueActionPayload.SqsFifoQueueActionPayloadBuilder builder = SqsFifoQueueActionPayload.builder();
  39 + builder.msgType(payload.getMsgType());
  40 + builder.requestId(payload.getRequestId());
  41 + builder.queue(configuration.getQueue());
  42 + builder.deviceId(msg.getDeviceId().toString());
  43 + builder.msgBody(getMsgBody(ctx, msg));
  44 + return Optional.of(new SqsFifoQueueActionMsg(msg.getTenantId(),
  45 + msg.getCustomerId(),
  46 + msg.getDeviceId(),
  47 + builder.build()));
  48 + }
  49 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.fifo;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
  20 +
  21 +/**
  22 + * Created by Valerii Sosliuk on 11/10/2017.
  23 + */
  24 +@Data
  25 +public class SqsFifoQueuePluginActionConfiguration implements TemplateActionConfiguration {
  26 +
  27 + private String queue;
  28 + private String template;
  29 + private boolean sync;
  30 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.standard;
  17 +
  18 +import org.thingsboard.server.common.data.id.CustomerId;
  19 +import org.thingsboard.server.common.data.id.DeviceId;
  20 +import org.thingsboard.server.common.data.id.TenantId;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  22 +
  23 +/**
  24 + * Created by Valerii Sosliuk on 11/6/2017.
  25 + */
  26 +public class SqsStandardQueueActionMsg extends AbstractRuleToPluginMsg<SqsStandardQueueActionPayload> {
  27 +
  28 + public SqsStandardQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsStandardQueueActionPayload payload) {
  29 + super(tenantId, customerId, deviceId, payload);
  30 + }
  31 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.standard;
  17 +
  18 +import lombok.Builder;
  19 +import lombok.Data;
  20 +import org.thingsboard.server.common.msg.session.MsgType;
  21 +
  22 +import java.io.Serializable;
  23 +
  24 +/**
  25 + * Created by Valerii Sosliuk on 11/6/2017.
  26 + */
  27 +@Data
  28 +@Builder
  29 +public class SqsStandardQueueActionPayload implements Serializable {
  30 +
  31 + private final String queue;
  32 + private final String msgBody;
  33 + private final int delaySeconds;
  34 +
  35 + private final Integer requestId;
  36 + private final MsgType msgType;
  37 + private final boolean sync;
  38 +
  39 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.standard;
  17 +
  18 +import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
  19 +import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
  20 +import org.thingsboard.server.extensions.api.component.Action;
  21 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  22 +import org.thingsboard.server.extensions.api.rules.RuleContext;
  23 +import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
  24 +
  25 +import java.util.Optional;
  26 +
  27 +/**
  28 + * Created by Valerii Sosliuk on 11/5/2017.
  29 + */
  30 +@Action(name = "SQS Standard Queue Action", descriptor = "SqsStandardQueueActionDescriptor.json", configuration = SqsStandardQueuePluginActionConfiguration.class)
  31 +public class SqsStandardQueuePluginAction extends AbstractTemplatePluginAction<SqsStandardQueuePluginActionConfiguration> {
  32 +
  33 + @Override
  34 + protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
  35 + SqsStandardQueueActionPayload.SqsStandardQueueActionPayloadBuilder builder = SqsStandardQueueActionPayload.builder();
  36 + builder.msgType(payload.getMsgType());
  37 + builder.requestId(payload.getRequestId());
  38 + builder.queue(configuration.getQueue());
  39 + builder.delaySeconds(configuration.getDelaySeconds());
  40 + builder.msgBody(getMsgBody(ctx, msg));
  41 + return Optional.of(new SqsStandardQueueActionMsg(msg.getTenantId(),
  42 + msg.getCustomerId(),
  43 + msg.getDeviceId(),
  44 + builder.build()));
  45 + }
  46 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.action.standard;
  17 +
  18 +import lombok.Data;
  19 +import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
  20 +
  21 +/**
  22 + * Created by Valerii Sosliuk on 11/6/2017.
  23 + */
  24 +@Data
  25 +public class SqsStandardQueuePluginActionConfiguration implements TemplateActionConfiguration {
  26 +
  27 + private String queue;
  28 + private int delaySeconds;
  29 + private boolean sync;
  30 + private String template;
  31 +
  32 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.plugin;
  17 +
  18 +import com.amazonaws.services.sqs.AmazonSQS;
  19 +import com.amazonaws.services.sqs.model.SendMessageRequest;
  20 +import com.amazonaws.services.sqs.model.SendMessageResult;
  21 +import lombok.RequiredArgsConstructor;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.thingsboard.server.common.data.id.RuleId;
  24 +import org.thingsboard.server.common.data.id.TenantId;
  25 +import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
  26 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  27 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  28 +import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
  29 +import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
  30 +import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
  31 +import org.thingsboard.server.extensions.api.rules.RuleException;
  32 +import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionMsg;
  33 +import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionPayload;
  34 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
  35 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
  36 +
  37 +/**
  38 + * Created by Valerii Sosliuk on 11/15/2017.
  39 + */
  40 +@RequiredArgsConstructor
  41 +@Slf4j
  42 +public class SqsMessageHandler implements RuleMsgHandler {
  43 +
  44 + private final AmazonSQS sqs;
  45 +
  46 + @Override
  47 + public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
  48 + if (msg instanceof SqsStandardQueueActionMsg) {
  49 + sendMessageToStandardQueue(ctx, tenantId, ruleId, msg);
  50 + return;
  51 + }
  52 + if (msg instanceof SqsFifoQueueActionMsg) {
  53 + sendMessageToFifoQueue(ctx, tenantId, ruleId, msg);
  54 + return;
  55 + }
  56 + throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
  57 + }
  58 +
  59 + private void sendMessageToStandardQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
  60 + SqsStandardQueueActionPayload payload = ((SqsStandardQueueActionMsg) msg).getPayload();
  61 + SendMessageRequest sendMsgRequest = new SendMessageRequest()
  62 + .withDelaySeconds(payload.getDelaySeconds())
  63 + .withQueueUrl(payload.getQueue())
  64 + .withMessageBody(payload.getMsgBody());
  65 + sqs.sendMessage(sendMsgRequest);
  66 + if (payload.isSync()) {
  67 + ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
  68 + BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
  69 + }
  70 + }
  71 +
  72 + private void sendMessageToFifoQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
  73 + SqsFifoQueueActionPayload payload = ((SqsFifoQueueActionMsg) msg).getPayload();
  74 + SendMessageRequest sendMsgRequest = new SendMessageRequest()
  75 + .withQueueUrl(payload.getQueue())
  76 + .withMessageBody(payload.getMsgBody())
  77 + .withMessageGroupId(payload.getDeviceId());
  78 + sqs.sendMessage(sendMsgRequest);
  79 + if (payload.isSync()) {
  80 + ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
  81 + BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
  82 + }
  83 + }
  84 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.plugin;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.regions.Regions;
  22 +import com.amazonaws.services.sqs.AmazonSQS;
  23 +import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
  24 +import org.thingsboard.server.extensions.api.component.Plugin;
  25 +import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
  26 +import org.thingsboard.server.extensions.api.plugins.PluginContext;
  27 +import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
  28 +import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueuePluginAction;
  29 +import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginAction;
  30 +
  31 +/**
  32 + * Created by Valerii Sosliuk on 11/6/2017.
  33 + */
  34 +@Plugin(name = "SQS Plugin", actions = {SqsStandardQueuePluginAction.class, SqsFifoQueuePluginAction.class},
  35 + descriptor = "SqsPluginDescriptor.json", configuration = SqsPluginConfiguration.class)
  36 +public class SqsPlugin extends AbstractPlugin<SqsPluginConfiguration> {
  37 +
  38 + private SqsMessageHandler sqsMessageHandler;
  39 + private SqsPluginConfiguration configuration;
  40 +
  41 + @Override
  42 + public void init(SqsPluginConfiguration configuration) {
  43 + this.configuration = configuration;
  44 + init();
  45 + }
  46 +
  47 + private void init() {
  48 + AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
  49 + AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
  50 + .withRegion(Regions.fromName(configuration.getRegion())).build();
  51 + this.sqsMessageHandler = new SqsMessageHandler(sqs);
  52 +
  53 + }
  54 +
  55 + private void destroy() {
  56 + this.sqsMessageHandler = null;
  57 + }
  58 +
  59 + @Override
  60 + protected RuleMsgHandler getRuleMsgHandler() {
  61 + return sqsMessageHandler;
  62 + }
  63 +
  64 + @Override
  65 + public void resume(PluginContext ctx) {
  66 + init();
  67 + }
  68 +
  69 + @Override
  70 + public void suspend(PluginContext ctx) {
  71 + destroy();
  72 + }
  73 +
  74 + @Override
  75 + public void stop(PluginContext ctx) {
  76 + destroy();
  77 + }
  78 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs.plugin;
  17 +
  18 +import lombok.Data;
  19 +
  20 +/**
  21 + * Created by Valerii Sosliuk on 11/5/2017.
  22 + */
  23 +@Data
  24 +public class SqsPluginConfiguration {
  25 +
  26 + private String accessKeyId;
  27 + private String secretAccessKey;
  28 + private String region;
  29 +
  30 +}
  1 +{
  2 + "schema": {
  3 + "title": "SQS FIFO Queue Action Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "sync": {
  7 + "title": "Requires delivery confirmation",
  8 + "type": "boolean"
  9 + },
  10 + "queue": {
  11 + "title": "Queue URL",
  12 + "type": "string"
  13 + },
  14 + "template": {
  15 + "title": "Body Template",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "sync",
  21 + "queue",
  22 + "template"
  23 + ]
  24 + },
  25 + "form": [
  26 + "sync",
  27 + "queue",
  28 + {
  29 + "key": "template",
  30 + "type": "textarea",
  31 + "rows": 5
  32 + }
  33 + ]
  34 +}
  1 +{
  2 + "schema": {
  3 + "title": "SQS Plugin Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "accessKeyId": {
  7 + "title": "Access Key ID",
  8 + "type": "string"
  9 + },
  10 + "secretAccessKey": {
  11 + "title": "Secret Access Key",
  12 + "type": "string"
  13 + },
  14 + "region": {
  15 + "title": "Region",
  16 + "type": "string"
  17 + }
  18 + },
  19 + "required": [
  20 + "accessKeyId",
  21 + "secretAccessKey",
  22 + "region"
  23 + ]
  24 + },
  25 + "form": [
  26 + "accessKeyId",
  27 + "secretAccessKey",
  28 + "region"
  29 + ]
  30 +}
  1 +{
  2 + "schema": {
  3 + "title": "SQS Standard Queue Action Configuration",
  4 + "type": "object",
  5 + "properties": {
  6 + "sync": {
  7 + "title": "Requires delivery confirmation",
  8 + "type": "boolean"
  9 + },
  10 + "queue": {
  11 + "title": "Queue URL",
  12 + "type": "string"
  13 + },
  14 + "delaySeconds": {
  15 + "title": "Delay Seconds",
  16 + "type": "integer",
  17 + "default": 0
  18 + },
  19 + "template": {
  20 + "title": "Body Template",
  21 + "type": "string"
  22 + }
  23 + },
  24 + "required": [
  25 + "sync",
  26 + "queue",
  27 + "delaySeconds",
  28 + "template"
  29 + ]
  30 + },
  31 + "form": [
  32 + "sync",
  33 + "queue",
  34 + "delaySeconds",
  35 + {
  36 + "key": "template",
  37 + "type": "textarea",
  38 + "rows": 5
  39 + }
  40 + ]
  41 +}
  1 +/**
  2 + * Copyright © 2016-2017 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.extensions.sqs;
  17 +
  18 +import com.amazonaws.auth.AWSCredentials;
  19 +import com.amazonaws.auth.AWSStaticCredentialsProvider;
  20 +import com.amazonaws.auth.BasicAWSCredentials;
  21 +import com.amazonaws.regions.Regions;
  22 +import com.amazonaws.services.sqs.AmazonSQS;
  23 +import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
  24 +import com.amazonaws.services.sqs.model.DeleteMessageRequest;
  25 +import com.amazonaws.services.sqs.model.Message;
  26 +import lombok.extern.slf4j.Slf4j;
  27 +
  28 +import java.util.List;
  29 +
  30 +/**
  31 + * Created by Valerii Sosliuk on 11/10/2017.
  32 + */
  33 +@Slf4j
  34 +public class SqsDemoClient {
  35 +
  36 + private static final String ACCESS_KEY_ID = "$ACCES_KEY_ID";
  37 + private static final String SECRET_ACCESS_KEY = "$SECRET_ACCESS_KEY";
  38 +
  39 + private static final String QUEUE_URL = "$QUEUE_URL";
  40 + private static final String REGION = "us-east-1";
  41 +
  42 + public static void main(String[] args) {
  43 + log.info("Starting SQS Demo Clinent...");
  44 + AWSCredentials awsCredentials = new BasicAWSCredentials(ACCESS_KEY_ID, SECRET_ACCESS_KEY);
  45 + AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
  46 + .withRegion(Regions.fromName(REGION)).build();
  47 + SqsDemoClient client = new SqsDemoClient();
  48 + client.pollMessages(sqs);
  49 + }
  50 +
  51 + private void pollMessages(AmazonSQS sqs) {
  52 + log.info("Polling messages");
  53 + while (true) {
  54 + List<Message> messages = sqs.receiveMessage(QUEUE_URL).getMessages();
  55 + messages.forEach(m -> {
  56 + log.info("Message Received: " + m.getBody());
  57 + System.out.println(m.getBody());
  58 + DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest(QUEUE_URL, m.getReceiptHandle());
  59 + sqs.deleteMessage(deleteMessageRequest);
  60 + });
  61 + try {
  62 + Thread.sleep(1000);
  63 + } catch (InterruptedException e) {
  64 + Thread.currentThread().interrupt();
  65 + e.printStackTrace();
  66 + }
  67 + }
  68 + }
  69 +}
  1 +<configuration>
  2 + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  3 + <encoder>
  4 + <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
  5 + </encoder>
  6 + </appender>
  7 + <root level="INFO">
  8 + <appender-ref ref="STDOUT"/>
  9 + </root>
  10 +</configuration>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>pom</packaging> 28 <packaging>pom</packaging>
29 29
30 <name>Thingsboard Extensions</name> 30 <name>Thingsboard Extensions</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <main.dir>${basedir}/..</main.dir> 34 <main.dir>${basedir}/..</main.dir>
@@ -39,6 +39,8 @@ @@ -39,6 +39,8 @@
39 <module>extension-rest-api-call</module> 39 <module>extension-rest-api-call</module>
40 <module>extension-kafka</module> 40 <module>extension-kafka</module>
41 <module>extension-mqtt</module> 41 <module>extension-mqtt</module>
  42 + <module>extension-sqs</module>
  43 + <module>extension-sns</module>
42 </modules> 44 </modules>
43 45
44 </project> 46 </project>
@@ -351,6 +351,18 @@ @@ -351,6 +351,18 @@
351 <version>${project.version}</version> 351 <version>${project.version}</version>
352 </dependency> 352 </dependency>
353 <dependency> 353 <dependency>
  354 + <groupId>org.thingsboard.extensions</groupId>
  355 + <artifactId>extension-sqs</artifactId>
  356 + <classifier>extension</classifier>
  357 + <version>${project.version}</version>
  358 + </dependency>
  359 + <dependency>
  360 + <groupId>org.thingsboard.extensions</groupId>
  361 + <artifactId>extension-sns</artifactId>
  362 + <classifier>extension</classifier>
  363 + <version>${project.version}</version>
  364 + </dependency>
  365 + <dependency>
354 <groupId>org.thingsboard.common</groupId> 366 <groupId>org.thingsboard.common</groupId>
355 <artifactId>data</artifactId> 367 <artifactId>data</artifactId>
356 <version>${project.version}</version> 368 <version>${project.version}</version>
  1 +@REM
  2 +@REM Copyright © 2016-2017 The Thingsboard Authors
  3 +@REM
  4 +@REM Licensed under the Apache License, Version 2.0 (the "License");
  5 +@REM you may not use this file except in compliance with the License.
  6 +@REM You may obtain a copy of the License at
  7 +@REM
  8 +@REM http://www.apache.org/licenses/LICENSE-2.0
  9 +@REM
  10 +@REM Unless required by applicable law or agreed to in writing, software
  11 +@REM distributed under the License is distributed on an "AS IS" BASIS,
  12 +@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +@REM See the License for the specific language governing permissions and
  14 +@REM limitations under the License.
  15 +@REM
  16 +
  17 +mvn clean install -rf :application
  18 +
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server Tools</name> 30 <name>Thingsboard Server Tools</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -26,9 +26,16 @@ import org.springframework.http.client.ClientHttpResponse; @@ -26,9 +26,16 @@ import org.springframework.http.client.ClientHttpResponse;
26 import org.springframework.http.client.support.HttpRequestWrapper; 26 import org.springframework.http.client.support.HttpRequestWrapper;
27 import org.springframework.web.client.HttpClientErrorException; 27 import org.springframework.web.client.HttpClientErrorException;
28 import org.springframework.web.client.RestTemplate; 28 import org.springframework.web.client.RestTemplate;
  29 +import org.thingsboard.server.common.data.Customer;
29 import org.thingsboard.server.common.data.Device; 30 import org.thingsboard.server.common.data.Device;
  31 +import org.thingsboard.server.common.data.alarm.Alarm;
  32 +import org.thingsboard.server.common.data.alarm.AlarmSeverity;
  33 +import org.thingsboard.server.common.data.alarm.AlarmStatus;
  34 +import org.thingsboard.server.common.data.asset.Asset;
  35 +import org.thingsboard.server.common.data.id.AssetId;
30 import org.thingsboard.server.common.data.id.CustomerId; 36 import org.thingsboard.server.common.data.id.CustomerId;
31 import org.thingsboard.server.common.data.id.DeviceId; 37 import org.thingsboard.server.common.data.id.DeviceId;
  38 +import org.thingsboard.server.common.data.id.EntityId;
32 import org.thingsboard.server.common.data.security.DeviceCredentials; 39 import org.thingsboard.server.common.data.security.DeviceCredentials;
33 40
34 import java.io.IOException; 41 import java.io.IOException;
@@ -71,18 +78,40 @@ public class RestClient implements ClientHttpRequestInterceptor { @@ -71,18 +78,40 @@ public class RestClient implements ClientHttpRequestInterceptor {
71 } 78 }
72 } 79 }
73 80
74 - public Device createDevice(String name) { 81 + public Customer createCustomer(String title) {
  82 + Customer customer = new Customer();
  83 + customer.setTitle(title);
  84 + return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
  85 + }
  86 +
  87 + public Device createDevice(String name, String type) {
75 Device device = new Device(); 88 Device device = new Device();
76 device.setName(name); 89 device.setName(name);
  90 + device.setType(type);
77 return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody(); 91 return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
78 } 92 }
79 93
  94 + public Asset createAsset(String name, String type) {
  95 + Asset asset = new Asset();
  96 + asset.setName(name);
  97 + asset.setType(type);
  98 + return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
  99 + }
  100 +
  101 + public Alarm createAlarm(Alarm alarm) {
  102 + return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
  103 + }
80 104
81 public Device assignDevice(CustomerId customerId, DeviceId deviceId) { 105 public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
82 return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class, 106 return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
83 customerId.toString(), deviceId.toString()).getBody(); 107 customerId.toString(), deviceId.toString()).getBody();
84 } 108 }
85 109
  110 + public Asset assignAsset(CustomerId customerId, AssetId assetId) {
  111 + return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class,
  112 + customerId.toString(), assetId.toString()).getBody();
  113 + }
  114 +
86 public DeviceCredentials getCredentials(DeviceId id) { 115 public DeviceCredentials getCredentials(DeviceId id) {
87 return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody(); 116 return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
88 } 117 }
@@ -91,11 +120,14 @@ public class RestClient implements ClientHttpRequestInterceptor { @@ -91,11 +120,14 @@ public class RestClient implements ClientHttpRequestInterceptor {
91 return restTemplate; 120 return restTemplate;
92 } 121 }
93 122
  123 + public String getToken() {
  124 + return token;
  125 + }
  126 +
94 @Override 127 @Override
95 public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException { 128 public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
96 HttpRequest wrapper = new HttpRequestWrapper(request); 129 HttpRequest wrapper = new HttpRequestWrapper(request);
97 wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); 130 wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
98 return execution.execute(wrapper, bytes); 131 return execution.execute(wrapper, bytes);
99 } 132 }
100 -  
101 -} 133 +}
  1 +# -*- coding: utf-8 -*-
  2 +#
  3 +# Copyright © 2016-2017 The Thingsboard Authors
  4 +#
  5 +# Licensed under the Apache License, Version 2.0 (the "License");
  6 +# you may not use this file except in compliance with the License.
  7 +# You may obtain a copy of the License at
  8 +#
  9 +# http://www.apache.org/licenses/LICENSE-2.0
  10 +#
  11 +# Unless required by applicable law or agreed to in writing, software
  12 +# distributed under the License is distributed on an "AS IS" BASIS,
  13 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +# See the License for the specific language governing permissions and
  15 +# limitations under the License.
  16 +#
  17 +
  18 +import paho.mqtt.client as mqtt
  19 +from time import sleep
  20 +import random
  21 +
  22 +broker="test.mosquitto.org"
  23 +topic_pub='v1/devices/me/telemetry'
  24 +
  25 +
  26 +client = mqtt.Client()
  27 +
  28 +client.username_pw_set("TEST_TOKEN")
  29 +client.connect('127.0.0.1', 1883, 1)
  30 +
  31 +for i in range(5):
  32 + x = random.randrange(20, 100)
  33 + print x
  34 + msg = '{"windSpeed":"'+ str(x) + '"}'
  35 + client.publish(topic_pub, msg)
  36 + sleep(0.1)
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard COAP Transport</name> 30 <name>Thingsboard COAP Transport</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard HTTP Transport</name> 30 <name>Thingsboard HTTP Transport</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard MQTT Transport</name> 30 <name>Thingsboard MQTT Transport</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -229,6 +229,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @@ -229,6 +229,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
229 } else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) { 229 } else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
230 deviceSessionCtx.setAllowAttributeResponses(); 230 deviceSessionCtx.setAllowAttributeResponses();
231 grantedQoSList.add(getMinSupportedQos(reqQoS)); 231 grantedQoSList.add(getMinSupportedQos(reqQoS));
  232 + } else if (topicName.equals(GATEWAY_ATTRIBUTES_TOPIC)) {
  233 + grantedQoSList.add(getMinSupportedQos(reqQoS));
232 } else { 234 } else {
233 log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS); 235 log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
234 grantedQoSList.add(FAILURE.value()); 236 grantedQoSList.add(FAILURE.value());
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 package org.thingsboard.server.transport.mqtt.session; 16 package org.thingsboard.server.transport.mqtt.session;
17 17
18 import com.google.gson.Gson; 18 import com.google.gson.Gson;
  19 +import com.google.gson.JsonArray;
19 import com.google.gson.JsonElement; 20 import com.google.gson.JsonElement;
20 import com.google.gson.JsonObject; 21 import com.google.gson.JsonObject;
21 import io.netty.buffer.ByteBuf; 22 import io.netty.buffer.ByteBuf;
@@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator; @@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
24 import io.netty.handler.codec.mqtt.*; 25 import io.netty.handler.codec.mqtt.*;
25 import org.thingsboard.server.common.data.Device; 26 import org.thingsboard.server.common.data.Device;
26 import org.thingsboard.server.common.data.id.SessionId; 27 import org.thingsboard.server.common.data.id.SessionId;
  28 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
27 import org.thingsboard.server.common.data.kv.KvEntry; 29 import org.thingsboard.server.common.data.kv.KvEntry;
28 import org.thingsboard.server.common.msg.core.*; 30 import org.thingsboard.server.common.msg.core.*;
29 import org.thingsboard.server.common.msg.kv.AttributesKVMsg; 31 import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
@@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics; @@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics;
35 import org.thingsboard.server.transport.mqtt.MqttTransportHandler; 37 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
36 38
37 import java.nio.charset.Charset; 39 import java.nio.charset.Charset;
  40 +import java.util.List;
38 import java.util.Optional; 41 import java.util.Optional;
39 import java.util.concurrent.atomic.AtomicInteger; 42 import java.util.concurrent.atomic.AtomicInteger;
40 43
@@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext { @@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
83 if (responseMsg.isSuccess()) { 86 if (responseMsg.isSuccess()) {
84 MsgType requestMsgType = responseMsg.getRequestMsgType(); 87 MsgType requestMsgType = responseMsg.getRequestMsgType();
85 Integer requestId = responseMsg.getRequestId(); 88 Integer requestId = responseMsg.getRequestId();
86 - if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) { 89 + if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
87 return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId)); 90 return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
88 } 91 }
89 } 92 }
@@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext { @@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
135 if (responseData.isPresent()) { 138 if (responseData.isPresent()) {
136 AttributesKVMsg msg = responseData.get(); 139 AttributesKVMsg msg = responseData.get();
137 if (msg.getClientAttributes() != null) { 140 if (msg.getClientAttributes() != null) {
138 - msg.getClientAttributes().forEach(v -> addValueToJson(result, "value", v)); 141 + addValues(result, msg.getClientAttributes());
139 } 142 }
140 if (msg.getSharedAttributes() != null) { 143 if (msg.getSharedAttributes() != null) {
141 - msg.getSharedAttributes().forEach(v -> addValueToJson(result, "value", v)); 144 + addValues(result, msg.getSharedAttributes());
142 } 145 }
143 } 146 }
144 return createMqttPublishMsg(topic, result); 147 return createMqttPublishMsg(topic, result);
145 } 148 }
146 149
  150 + private void addValues(JsonObject result, List<AttributeKvEntry> kvList) {
  151 + if (kvList.size() == 1) {
  152 + addValueToJson(result, "value", kvList.get(0));
  153 + } else {
  154 + JsonObject values;
  155 + if (result.has("values")) {
  156 + values = result.get("values").getAsJsonObject();
  157 + } else {
  158 + values = new JsonObject();
  159 + result.add("values", values);
  160 + }
  161 + kvList.forEach(value -> addValueToJson(values, value.getKey(), value));
  162 + }
  163 + }
  164 +
147 private void addValueToJson(JsonObject json, String name, KvEntry entry) { 165 private void addValueToJson(JsonObject json, String name, KvEntry entry) {
148 switch (entry.getDataType()) { 166 switch (entry.getDataType()) {
149 case BOOLEAN: 167 case BOOLEAN:
150 - Optional<Boolean> booleanValue = entry.getBooleanValue();  
151 - if (booleanValue.isPresent()) {  
152 - json.addProperty(name, booleanValue.get());  
153 - } 168 + entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean));
154 break; 169 break;
155 case STRING: 170 case STRING:
156 - Optional<String> stringValue = entry.getStrValue();  
157 - if (stringValue.isPresent()) {  
158 - json.addProperty(name, stringValue.get());  
159 - } 171 + entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString));
160 break; 172 break;
161 case DOUBLE: 173 case DOUBLE:
162 - Optional<Double> doubleValue = entry.getDoubleValue();  
163 - if (doubleValue.isPresent()) {  
164 - json.addProperty(name, doubleValue.get());  
165 - } 174 + entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble));
166 break; 175 break;
167 case LONG: 176 case LONG:
168 - Optional<Long> longValue = entry.getLongValue();  
169 - if (longValue.isPresent()) {  
170 - json.addProperty(name, longValue.get());  
171 - } 177 + entry.getLongValue().ifPresent(aLong -> json.addProperty(name, aLong));
172 break; 178 break;
173 } 179 }
174 } 180 }
@@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService; @@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService;
41 import org.thingsboard.server.transport.mqtt.MqttTransportHandler; 41 import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
42 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; 42 import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
43 43
44 -import java.util.Collections;  
45 -import java.util.HashMap;  
46 -import java.util.Map;  
47 -import java.util.Optional; 44 +import java.util.*;
48 import java.util.stream.Collectors; 45 import java.util.stream.Collectors;
49 46
50 import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload; 47 import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload;
@@ -186,24 +183,34 @@ public class GatewaySessionCtx { @@ -186,24 +183,34 @@ public class GatewaySessionCtx {
186 } 183 }
187 } 184 }
188 185
189 - public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException {  
190 - JsonElement json = validateJsonPayload(gatewaySessionId, mqttMsg.payload()); 186 + public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException {
  187 + JsonElement json = validateJsonPayload(gatewaySessionId, msg.payload());
191 if (json.isJsonObject()) { 188 if (json.isJsonObject()) {
192 JsonObject jsonObj = json.getAsJsonObject(); 189 JsonObject jsonObj = json.getAsJsonObject();
193 int requestId = jsonObj.get("id").getAsInt(); 190 int requestId = jsonObj.get("id").getAsInt();
194 String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); 191 String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
195 boolean clientScope = jsonObj.get("client").getAsBoolean(); 192 boolean clientScope = jsonObj.get("client").getAsBoolean();
196 - String key = jsonObj.get("key").getAsString(); 193 + Set<String> keys;
  194 + if (jsonObj.has("key")) {
  195 + keys = Collections.singleton(jsonObj.get("key").getAsString());
  196 + } else {
  197 + JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
  198 + keys = new HashSet<>();
  199 + for (JsonElement keyObj : keysArray) {
  200 + keys.add(keyObj.getAsString());
  201 + }
  202 + }
197 203
198 BasicGetAttributesRequest request; 204 BasicGetAttributesRequest request;
199 if (clientScope) { 205 if (clientScope) {
200 - request = new BasicGetAttributesRequest(requestId, Collections.singleton(key), null); 206 + request = new BasicGetAttributesRequest(requestId, keys, null);
201 } else { 207 } else {
202 - request = new BasicGetAttributesRequest(requestId, null, Collections.singleton(key)); 208 + request = new BasicGetAttributesRequest(requestId, null, keys);
203 } 209 }
204 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); 210 GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
205 processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(), 211 processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
206 new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request))); 212 new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
  213 + ack(msg);
207 } else { 214 } else {
208 throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); 215 throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
209 } 216 }
@@ -251,7 +258,7 @@ public class GatewaySessionCtx { @@ -251,7 +258,7 @@ public class GatewaySessionCtx {
251 } 258 }
252 259
253 private void ack(MqttPublishMessage msg) { 260 private void ack(MqttPublishMessage msg) {
254 - if(msg.variableHeader().messageId() > 0) { 261 + if (msg.variableHeader().messageId() > 0) {
255 writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId())); 262 writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId()));
256 } 263 }
257 } 264 }
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>pom</packaging> 28 <packaging>pom</packaging>
29 29
30 <name>Thingsboard Server Transport Modules</name> 30 <name>Thingsboard Server Transport Modules</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <main.dir>${basedir}/..</main.dir> 34 <main.dir>${basedir}/..</main.dir>
@@ -30,6 +30,7 @@ @@ -30,6 +30,7 @@
30 "angular-material": "1.1.1", 30 "angular-material": "1.1.1",
31 "angular-material-data-table": "^0.10.9", 31 "angular-material-data-table": "^0.10.9",
32 "angular-material-icons": "^0.7.1", 32 "angular-material-icons": "^0.7.1",
  33 + "angular-material-expansion-panel": "^0.7.2",
33 "angular-messages": "1.5.8", 34 "angular-messages": "1.5.8",
34 "angular-route": "1.5.8", 35 "angular-route": "1.5.8",
35 "angular-sanitize": "1.5.8", 36 "angular-sanitize": "1.5.8",
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 <packaging>jar</packaging> 28 <packaging>jar</packaging>
29 29
30 <name>Thingsboard Server UI</name> 30 <name>Thingsboard Server UI</name>
31 - <url>http://thingsboard.org</url> 31 + <url>https://thingsboard.io</url>
32 32
33 <properties> 33 <properties>
34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 34 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -65,8 +65,8 @@ function LoginService($http, $q) { @@ -65,8 +65,8 @@ function LoginService($http, $q) {
65 65
66 function sendResetPasswordLink(email) { 66 function sendResetPasswordLink(email) {
67 var deferred = $q.defer(); 67 var deferred = $q.defer();
68 - var url = '/api/noauth/resetPasswordByEmail?email=' + email;  
69 - $http.post(url, null).then(function success(response) { 68 + var url = '/api/noauth/resetPasswordByEmail';
  69 + $http.post(url, {email: email}).then(function success(response) {
70 deferred.resolve(response); 70 deferred.resolve(response);
71 }, function fail() { 71 }, function fail() {
72 deferred.reject(); 72 deferred.reject();
@@ -76,8 +76,8 @@ function LoginService($http, $q) { @@ -76,8 +76,8 @@ function LoginService($http, $q) {
76 76
77 function resetPassword(resetToken, password) { 77 function resetPassword(resetToken, password) {
78 var deferred = $q.defer(); 78 var deferred = $q.defer();
79 - var url = '/api/noauth/resetPassword?resetToken=' + resetToken + '&password=' + password;  
80 - $http.post(url, null).then(function success(response) { 79 + var url = '/api/noauth/resetPassword';
  80 + $http.post(url, {resetToken: resetToken, password: password}).then(function success(response) {
81 deferred.resolve(response); 81 deferred.resolve(response);
82 }, function fail() { 82 }, function fail() {
83 deferred.reject(); 83 deferred.reject();
@@ -87,8 +87,8 @@ function LoginService($http, $q) { @@ -87,8 +87,8 @@ function LoginService($http, $q) {
87 87
88 function activate(activateToken, password) { 88 function activate(activateToken, password) {
89 var deferred = $q.defer(); 89 var deferred = $q.defer();
90 - var url = '/api/noauth/activate?activateToken=' + activateToken + '&password=' + password;  
91 - $http.post(url, null).then(function success(response) { 90 + var url = '/api/noauth/activate';
  91 + $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
92 deferred.resolve(response); 92 deferred.resolve(response);
93 }, function fail() { 93 }, function fail() {
94 deferred.reject(); 94 deferred.reject();
@@ -98,8 +98,8 @@ function LoginService($http, $q) { @@ -98,8 +98,8 @@ function LoginService($http, $q) {
98 98
99 function changePassword(currentPassword, newPassword) { 99 function changePassword(currentPassword, newPassword) {
100 var deferred = $q.defer(); 100 var deferred = $q.defer();
101 - var url = '/api/auth/changePassword?currentPassword=' + currentPassword + '&newPassword=' + newPassword;  
102 - $http.post(url, null).then(function success(response) { 101 + var url = '/api/auth/changePassword';
  102 + $http.post(url, {currentPassword: currentPassword, newPassword: newPassword}).then(function success(response) {
103 deferred.resolve(response); 103 deferred.resolve(response);
104 }, function fail() { 104 }, function fail() {
105 deferred.reject(); 105 deferred.reject();
@@ -302,7 +302,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -302,7 +302,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
302 $rootScope.forceFullscreen = true; 302 $rootScope.forceFullscreen = true;
303 fetchAllowedDashboardIds(); 303 fetchAllowedDashboardIds();
304 } else if (currentUser.userId) { 304 } else if (currentUser.userId) {
305 - getUser(currentUser.userId).then( 305 + getUser(currentUser.userId, true).then(
306 function success(user) { 306 function success(user) {
307 currentUserDetails = user; 307 currentUserDetails = user;
308 updateUserLang(); 308 updateUserLang();
@@ -319,6 +319,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -319,6 +319,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
319 }, 319 },
320 function fail() { 320 function fail() {
321 deferred.reject(); 321 deferred.reject();
  322 + logout();
322 } 323 }
323 ) 324 )
324 } else { 325 } else {
@@ -414,19 +415,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -414,19 +415,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
414 } 415 }
415 $http.post(url, user).then(function success(response) { 416 $http.post(url, user).then(function success(response) {
416 deferred.resolve(response.data); 417 deferred.resolve(response.data);
417 - }, function fail(response) {  
418 - deferred.reject(response.data); 418 + }, function fail() {
  419 + deferred.reject();
419 }); 420 });
420 return deferred.promise; 421 return deferred.promise;
421 } 422 }
422 423
423 - function getUser(userId) { 424 + function getUser(userId, ignoreErrors) {
424 var deferred = $q.defer(); 425 var deferred = $q.defer();
425 var url = '/api/user/' + userId; 426 var url = '/api/user/' + userId;
426 - $http.get(url).then(function success(response) { 427 + $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
427 deferred.resolve(response.data); 428 deferred.resolve(response.data);
428 - }, function fail(response) {  
429 - deferred.reject(response.data); 429 + }, function fail() {
  430 + deferred.reject();
430 }); 431 });
431 return deferred.promise; 432 return deferred.promise;
432 } 433 }
@@ -436,8 +437,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi @@ -436,8 +437,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
436 var url = '/api/user/' + userId; 437 var url = '/api/user/' + userId;
437 $http.delete(url).then(function success() { 438 $http.delete(url).then(function success() {
438 deferred.resolve(); 439 deferred.resolve();
439 - }, function fail(response) {  
440 - deferred.reject(response.data); 440 + }, function fail() {
  441 + deferred.reject();
441 }); 442 });
442 return deferred.promise; 443 return deferred.promise;
443 } 444 }
@@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive'; @@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; 21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
22 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; 22 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
23 import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; 23 import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
  24 +import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
24 25
25 import thingsboardRpcWidgets from '../widget/lib/rpc'; 26 import thingsboardRpcWidgets from '../widget/lib/rpc';
26 27
@@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant'; @@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant';
42 import thingsboardUtils from '../common/utils.service'; 43 import thingsboardUtils from '../common/utils.service';
43 44
44 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, 45 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
45 - thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils]) 46 + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
46 .factory('widgetService', WidgetService) 47 .factory('widgetService', WidgetService)
47 .name; 48 .name;
48 49
@@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router'; @@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router';
39 import angularJwt from 'angular-jwt'; 39 import angularJwt from 'angular-jwt';
40 import 'angular-drag-and-drop-lists'; 40 import 'angular-drag-and-drop-lists';
41 import mdDataTable from 'angular-material-data-table'; 41 import mdDataTable from 'angular-material-data-table';
  42 +import 'angular-material-expansion-panel';
42 import ngTouch from 'angular-touch'; 43 import ngTouch from 'angular-touch';
43 import 'angular-carousel'; 44 import 'angular-carousel';
44 import 'clipboard'; 45 import 'clipboard';
@@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css'; @@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css';
82 import 'mdPickers/dist/mdPickers.min.css'; 83 import 'mdPickers/dist/mdPickers.min.css';
83 import 'angular-hotkeys/build/hotkeys.min.css'; 84 import 'angular-hotkeys/build/hotkeys.min.css';
84 import 'angular-carousel/dist/angular-carousel.min.css'; 85 import 'angular-carousel/dist/angular-carousel.min.css';
  86 +import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
85 import '../scss/main.scss'; 87 import '../scss/main.scss';
86 88
87 import AppConfig from './app.config'; 89 import AppConfig from './app.config';
@@ -103,6 +105,7 @@ angular.module('thingsboard', [ @@ -103,6 +105,7 @@ angular.module('thingsboard', [
103 angularJwt, 105 angularJwt,
104 'dndLists', 106 'dndLists',
105 mdDataTable, 107 mdDataTable,
  108 + 'material.components.expansionPanels',
106 ngTouch, 109 ngTouch,
107 'angular-carousel', 110 'angular-carousel',
108 'ngclipboard', 111 'ngclipboard',
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 --> 17 -->
18 <div flex layout="column" style="margin-top: -10px;"> 18 <div flex layout="column" style="margin-top: -10px;">
19 - <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div> 19 + <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
20 <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div> 20 <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
21 <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div> 21 <div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
22 </div> 22 </div>
@@ -77,8 +77,8 @@ export default function AssignAssetToCustomerController(customerService, assetSe @@ -77,8 +77,8 @@ export default function AssignAssetToCustomerController(customerService, assetSe
77 77
78 function assign() { 78 function assign() {
79 var tasks = []; 79 var tasks = [];
80 - for (var assetId in assetIds) {  
81 - tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId])); 80 + for (var i=0;i<assetIds.length;i++) {
  81 + tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[i]));
82 } 82 }
83 $q.all(tasks).then(function () { 83 $q.all(tasks).then(function () {
84 $mdDialog.hide(); 84 $mdDialog.hide();
@@ -425,12 +425,26 @@ function DashboardUtils(types, utils, timeService) { @@ -425,12 +425,26 @@ function DashboardUtils(types, utils, timeService) {
425 var prevColumns = prevGridSettings ? prevGridSettings.columns : 24; 425 var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
426 var ratio = gridSettings.columns / prevColumns; 426 var ratio = gridSettings.columns / prevColumns;
427 layout.gridSettings = gridSettings; 427 layout.gridSettings = gridSettings;
  428 + var maxRow = 0;
428 for (var w in layout.widgets) { 429 for (var w in layout.widgets) {
429 var widget = layout.widgets[w]; 430 var widget = layout.widgets[w];
  431 + maxRow = Math.max(maxRow, widget.row + widget.sizeY);
  432 + }
  433 + var newMaxRow = Math.round(maxRow * ratio);
  434 + for (w in layout.widgets) {
  435 + widget = layout.widgets[w];
  436 + if (widget.row + widget.sizeY == maxRow) {
  437 + widget.row = Math.round(widget.row * ratio);
  438 + widget.sizeY = newMaxRow - widget.row;
  439 + } else {
  440 + widget.row = Math.round(widget.row * ratio);
  441 + widget.sizeY = Math.round(widget.sizeY * ratio);
  442 + }
430 widget.sizeX = Math.round(widget.sizeX * ratio); 443 widget.sizeX = Math.round(widget.sizeX * ratio);
431 - widget.sizeY = Math.round(widget.sizeY * ratio);  
432 widget.col = Math.round(widget.col * ratio); 444 widget.col = Math.round(widget.col * ratio);
433 - widget.row = Math.round(widget.row * ratio); 445 + if (widget.col + widget.sizeX > gridSettings.columns) {
  446 + widget.sizeX = gridSettings.columns - widget.col;
  447 + }
434 } 448 }
435 } 449 }
436 450
@@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', []) @@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
317 name: "event.type-stats" 317 name: "event.type-stats"
318 } 318 }
319 }, 319 },
  320 + extensionType: {
  321 + http: "HTTP",
  322 + mqtt: "MQTT",
  323 + opc: "OPC UA"
  324 + },
  325 + extensionValueType: {
  326 + string: 'value.string',
  327 + long: 'value.long',
  328 + double: 'value.double',
  329 + boolean: 'value.boolean'
  330 + },
  331 + extensionTransformerType: {
  332 + toDouble: 'extension.to-double',
  333 + custom: 'extension.custom'
  334 + },
  335 + mqttConverterTypes: {
  336 + json: 'extension.converter-json',
  337 + custom: 'extension.custom'
  338 + },
  339 + mqttCredentialTypes: {
  340 + anonymous: {
  341 + value: "anonymous",
  342 + name: "extension.anonymous"
  343 + },
  344 + basic: {
  345 + value: "basic",
  346 + name: "extension.basic"
  347 + },
  348 + pem: {
  349 + value: "cert.PEM",
  350 + name: "extension.pem"
  351 + }
  352 + },
  353 + extensionOpcSecurityTypes: {
  354 + Basic128Rsa15: "Basic128Rsa15",
  355 + Basic256: "Basic256",
  356 + Basic256Sha256: "Basic256Sha256",
  357 + None: "None"
  358 + },
  359 + extensionIdentityType: {
  360 + anonymous: "extension.anonymous",
  361 + username: "extension.username"
  362 + },
  363 + extensionKeystoreType: {
  364 + PKCS12: "PKCS12",
  365 + JKS: "JKS"
  366 + },
320 latestTelemetry: { 367 latestTelemetry: {
321 value: "LATEST_TELEMETRY", 368 value: "LATEST_TELEMETRY",
322 name: "attribute.scope-latest-telemetry", 369 name: "attribute.scope-latest-telemetry",
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', []) @@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
18 .name; 18 .name;
19 19
20 /*@ngInject*/ 20 /*@ngInject*/
21 -function ConfirmOnExit($state, $mdDialog, $window, $filter) { 21 +function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
22 return { 22 return {
23 link: function ($scope) { 23 link: function ($scope) {
24 24
25 $window.onbeforeunload = function () { 25 $window.onbeforeunload = function () {
26 - if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) { 26 + if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
27 return $filter('translate')('confirm-on-exit.message'); 27 return $filter('translate')('confirm-on-exit.message');
28 } 28 }
29 } 29 }
30 $scope.$on('$stateChangeStart', function (event, next, current, params) { 30 $scope.$on('$stateChangeStart', function (event, next, current, params) {
31 - if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) { 31 + if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
32 event.preventDefault(); 32 event.preventDefault();
33 var confirm = $mdDialog.confirm() 33 var confirm = $mdDialog.confirm()
34 .title($filter('translate')('confirm-on-exit.title')) 34 .title($filter('translate')('confirm-on-exit.title'))
@@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
140 vm.widgetLayoutInfo = { 140 vm.widgetLayoutInfo = {
141 }; 141 };
142 142
  143 + vm.widgetIds = [];
  144 +
143 vm.widgetItemMap = { 145 vm.widgetItemMap = {
144 sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX', 146 sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
145 sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY', 147 sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
@@ -233,73 +235,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -233,73 +235,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
233 removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef 235 removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef
234 }); 236 });
235 237
236 - watchWidgets();  
237 -  
238 function onGridsterParentResize() { 238 function onGridsterParentResize() {
239 if (gridsterParent.height() && autofillHeight()) { 239 if (gridsterParent.height() && autofillHeight()) {
240 updateMobileOpts(); 240 updateMobileOpts();
241 } 241 }
242 } 242 }
243 243
244 - function watchWidgets() {  
245 - $scope.widgetsCollectionWatch = $scope.$watchCollection('vm.widgets', function () {  
246 - if (vm.skipInitialWidgetsWatch) {  
247 - $timeout(function() { vm.skipInitialWidgetsWatch = false; });  
248 - return;  
249 - }  
250 - var ids = [];  
251 - for (var i=0;i<vm.widgets.length;i++) {  
252 - var widget = vm.widgets[i];  
253 - if (!widget.id) {  
254 - widget.id = utils.guid();  
255 - }  
256 - ids.push(widget.id);  
257 - var layoutInfoObject = vm.widgetLayoutInfo[widget.id];  
258 - if (!layoutInfoObject) {  
259 - layoutInfoObject = {  
260 - widget: widget  
261 - };  
262 - Object.defineProperty(layoutInfoObject, 'sizeX', {  
263 - get: function() { return widgetSizeX(this.widget) },  
264 - set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}  
265 - });  
266 - Object.defineProperty(layoutInfoObject, 'sizeY', {  
267 - get: function() { return widgetSizeY(this.widget) },  
268 - set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}  
269 - });  
270 - Object.defineProperty(layoutInfoObject, 'row', {  
271 - get: function() { return widgetRow(this.widget) },  
272 - set: function(newRow) { setWidgetRow(this.widget, newRow)}  
273 - });  
274 - Object.defineProperty(layoutInfoObject, 'col', {  
275 - get: function() { return widgetCol(this.widget) },  
276 - set: function(newCol) { setWidgetCol(this.widget, newCol)}  
277 - });  
278 - vm.widgetLayoutInfo[widget.id] = layoutInfoObject;  
279 - }  
280 - }  
281 - for (var widgetId in vm.widgetLayoutInfo) {  
282 - if (ids.indexOf(widgetId) === -1) {  
283 - delete vm.widgetLayoutInfo[widgetId];  
284 - }  
285 - }  
286 - $mdUtil.nextTick(function () {  
287 - sortWidgets();  
288 - if (autofillHeight()) {  
289 - updateMobileOpts();  
290 - }  
291 - });  
292 - });  
293 - }  
294 -  
295 - function stopWatchWidgets() {  
296 - if ($scope.widgetsCollectionWatch) {  
297 - $scope.widgetsCollectionWatch();  
298 - $scope.widgetsCollectionWatch = null;  
299 - }  
300 - }  
301 -  
302 -  
303 //TODO: widgets visibility 244 //TODO: widgets visibility
304 /*gridsterParent.scroll(function () { 245 /*gridsterParent.scroll(function () {
305 updateVisibleRect(); 246 updateVisibleRect();
@@ -344,30 +285,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -344,30 +285,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
344 return isMobileSize; 285 return isMobileSize;
345 } 286 }
346 287
347 - $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {  
348 - updateMobileOpts();  
349 - });  
350 -  
351 - $scope.$watch('vm.isMobile', function () {  
352 - updateMobileOpts();  
353 - });  
354 -  
355 - $scope.$watch('vm.autofillHeight', function () {  
356 - updateMobileOpts();  
357 - });  
358 -  
359 - $scope.$watch('vm.mobileAutofillHeight', function () {  
360 - updateMobileOpts();  
361 - });  
362 -  
363 - $scope.$watch('vm.mobileRowHeight', function () {  
364 - updateMobileOpts();  
365 - });  
366 -  
367 - $scope.$watch('vm.isMobileDisabled', function () {  
368 - updateMobileOpts();  
369 - });  
370 -  
371 $scope.$watch('vm.columns', function () { 288 $scope.$watch('vm.columns', function () {
372 var columns = vm.columns ? vm.columns : 24; 289 var columns = vm.columns ? vm.columns : 24;
373 if (vm.gridsterOpts.columns != columns) { 290 if (vm.gridsterOpts.columns != columns) {
@@ -381,6 +298,19 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -381,6 +298,19 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
381 } 298 }
382 }); 299 });
383 300
  301 + $scope.$watch(function() {
  302 + return $mdMedia('gt-sm') + ',' + vm.isMobile + ',' + vm.isMobileDisabled;
  303 + }, function() {
  304 + updateMobileOpts();
  305 + sortWidgets();
  306 + });
  307 +
  308 + $scope.$watch(function() {
  309 + return vm.autofillHeight + ',' + vm.mobileAutofillHeight + ',' + vm.mobileRowHeight;
  310 + }, function () {
  311 + updateMobileOpts();
  312 + });
  313 +
384 $scope.$watch('vm.margins', function () { 314 $scope.$watch('vm.margins', function () {
385 var margins = vm.margins ? vm.margins : [10, 10]; 315 var margins = vm.margins ? vm.margins : [10, 10];
386 if (!angular.equals(vm.gridsterOpts.margins, margins)) { 316 if (!angular.equals(vm.gridsterOpts.margins, margins)) {
@@ -407,9 +337,70 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -407,9 +337,70 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
407 } 337 }
408 }); 338 });
409 339
  340 + $scope.$watchCollection('vm.widgets', function () {
  341 + var ids = [];
  342 + for (var i=0;i<vm.widgets.length;i++) {
  343 + var widget = vm.widgets[i];
  344 + if (!widget.id) {
  345 + widget.id = utils.guid();
  346 + }
  347 + ids.push(widget.id);
  348 + }
  349 + ids.sort(function (id1, id2) {
  350 + return id1.localeCompare(id2);
  351 + });
  352 + if (angular.equals(ids, vm.widgetIds)) {
  353 + return;
  354 + }
  355 + vm.widgetIds = ids;
  356 + for (i=0;i<vm.widgets.length;i++) {
  357 + widget = vm.widgets[i];
  358 + var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
  359 + if (!layoutInfoObject) {
  360 + layoutInfoObject = {
  361 + widget: widget
  362 + };
  363 + Object.defineProperty(layoutInfoObject, 'sizeX', {
  364 + get: function() { return widgetSizeX(this.widget) },
  365 + set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
  366 + });
  367 + Object.defineProperty(layoutInfoObject, 'sizeY', {
  368 + get: function() { return widgetSizeY(this.widget) },
  369 + set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
  370 + });
  371 + Object.defineProperty(layoutInfoObject, 'row', {
  372 + get: function() { return widgetRow(this.widget) },
  373 + set: function(newRow) { setWidgetRow(this.widget, newRow)}
  374 + });
  375 + Object.defineProperty(layoutInfoObject, 'col', {
  376 + get: function() { return widgetCol(this.widget) },
  377 + set: function(newCol) { setWidgetCol(this.widget, newCol)}
  378 + });
  379 + vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
  380 + }
  381 + }
  382 + for (var widgetId in vm.widgetLayoutInfo) {
  383 + if (ids.indexOf(widgetId) === -1) {
  384 + delete vm.widgetLayoutInfo[widgetId];
  385 + }
  386 + }
  387 + sortWidgets();
  388 + $mdUtil.nextTick(function () {
  389 + if (autofillHeight()) {
  390 + updateMobileOpts();
  391 + }
  392 + });
  393 + });
  394 +
  395 + $scope.$watch('vm.widgetLayouts', function () {
  396 + updateMobileOpts();
  397 + sortWidgets();
  398 + });
  399 +
410 $scope.$on('gridster-resized', function (event, sizes, theGridster) { 400 $scope.$on('gridster-resized', function (event, sizes, theGridster) {
411 if (checkIsLocalGridsterElement(theGridster)) { 401 if (checkIsLocalGridsterElement(theGridster)) {
412 vm.gridster = theGridster; 402 vm.gridster = theGridster;
  403 + setupGridster(vm.gridster);
413 vm.isResizing = false; 404 vm.isResizing = false;
414 //TODO: widgets visibility 405 //TODO: widgets visibility
415 //updateVisibleRect(false, true); 406 //updateVisibleRect(false, true);
@@ -419,6 +410,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -419,6 +410,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
419 $scope.$on('gridster-mobile-changed', function (event, theGridster) { 410 $scope.$on('gridster-mobile-changed', function (event, theGridster) {
420 if (checkIsLocalGridsterElement(theGridster)) { 411 if (checkIsLocalGridsterElement(theGridster)) {
421 vm.gridster = theGridster; 412 vm.gridster = theGridster;
  413 + setupGridster(vm.gridster);
422 detectRowSize(vm.gridster.isMobile).then( 414 detectRowSize(vm.gridster.isMobile).then(
423 function(rowHeight) { 415 function(rowHeight) {
424 if (vm.gridsterOpts.rowHeight != rowHeight) { 416 if (vm.gridsterOpts.rowHeight != rowHeight) {
@@ -517,18 +509,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -517,18 +509,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
517 loadDashboard(); 509 loadDashboard();
518 510
519 function sortWidgets() { 511 function sortWidgets() {
520 - stopWatchWidgets();  
521 vm.widgets.sort(function (widget1, widget2) { 512 vm.widgets.sort(function (widget1, widget2) {
522 var row1 = widgetOrder(widget1); 513 var row1 = widgetOrder(widget1);
523 var row2 = widgetOrder(widget2); 514 var row2 = widgetOrder(widget2);
524 var res = row1 - row2; 515 var res = row1 - row2;
525 if (res === 0) { 516 if (res === 0) {
526 - res = widget1.col - widget2.col; 517 + res = widgetCol(widget1) - widgetCol(widget2);
527 } 518 }
528 return res; 519 return res;
529 }); 520 });
530 - vm.skipInitialWidgetsWatch = true;  
531 - watchWidgets();  
532 } 521 }
533 522
534 function reload() { 523 function reload() {
@@ -1037,6 +1026,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -1037,6 +1026,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
1037 $scope.gridsterScopeWatcher = null; 1026 $scope.gridsterScopeWatcher = null;
1038 var gridsterScope = gridsterElement.scope(); 1027 var gridsterScope = gridsterElement.scope();
1039 vm.gridster = gridsterScope.gridster; 1028 vm.gridster = gridsterScope.gridster;
  1029 + setupGridster(vm.gridster);
1040 if (vm.onInit) { 1030 if (vm.onInit) {
1041 vm.onInit({dashboard: vm}); 1031 vm.onInit({dashboard: vm});
1042 } 1032 }
@@ -1046,6 +1036,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $ @@ -1046,6 +1036,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
1046 }); 1036 });
1047 } 1037 }
1048 1038
  1039 + function setupGridster(gridster) {
  1040 + if (gridster) {
  1041 + if (!gridster.origMoveOverlappingItems) {
  1042 + gridster.origMoveOverlappingItems = gridster.moveOverlappingItems;
  1043 + gridster.moveOverlappingItems = () => {};
  1044 + }
  1045 + }
  1046 + }
  1047 +
1049 function loading() { 1048 function loading() {
1050 return !vm.ignoreLoading && $rootScope.loading; 1049 return !vm.ignoreLoading && $rootScope.loading;
1051 } 1050 }
@@ -16,7 +16,10 @@ @@ -16,7 +16,10 @@
16 @import '../../scss/constants'; 16 @import '../../scss/constants';
17 17
18 .tb-details-title { 18 .tb-details-title {
19 - font-size: 1.600rem; 19 + font-size: 1.000rem;
  20 + @media (min-width: $layout-breakpoint-gt-sm) {
  21 + font-size: 1.600rem;
  22 + }
20 font-weight: 400; 23 font-weight: 400;
21 text-transform: uppercase; 24 text-transform: uppercase;
22 margin: 20px 8px 0 0; 25 margin: 20px 8px 0 0;
@@ -22,7 +22,7 @@ tb-js-func { @@ -22,7 +22,7 @@ tb-js-func {
22 border: 1px solid #C0C0C0; 22 border: 1px solid #C0C0C0;
23 height: 100%; 23 height: 100%;
24 #tb-javascript-input { 24 #tb-javascript-input {
25 - min-width: 400px; 25 + min-width: 200px;
26 min-height: 200px; 26 min-height: 200px;
27 width: 100%; 27 width: 100%;
28 height: 100%; 28 height: 100%;
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 <div layout="row" layout-align="start center" style="height: 40px;"> 19 <div layout="row" layout-align="start center" style="height: 40px;">
20 <span style="font-style: italic;">function({{ functionArgsString }}) {</span> 20 <span style="font-style: italic;">function({{ functionArgsString }}) {</span>
21 <span flex></span> 21 <span flex></span>
22 - <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> 22 + <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
23 </div> 23 </div>
24 <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column"> 24 <div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
25 <div flex id="tb-javascript-input" 25 <div flex id="tb-javascript-input"
@@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device @@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device
77 77
78 function assign() { 78 function assign() {
79 var tasks = []; 79 var tasks = [];
80 - for (var deviceId in deviceIds) {  
81 - tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId])); 80 + for (var i=0;i<deviceIds.length;i++) {
  81 + tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[i]));
82 } 82 }
83 $q.all(tasks).then(function () { 83 $q.all(tasks).then(function () {
84 $mdDialog.hide(); 84 $mdDialog.hide();
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 --> 17 -->
18 <div flex layout="column" style="margin-top: -10px;"> 18 <div flex layout="column" style="margin-top: -10px;">
19 - <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div> 19 + <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
20 <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div> 20 <div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
21 <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div> 21 <div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
22 </div> 22 </div>
@@ -67,4 +67,11 @@ @@ -67,4 +67,11 @@
67 entity-type="{{vm.types.entityType.device}}"> 67 entity-type="{{vm.types.entityType.device}}">
68 </tb-relation-table> 68 </tb-relation-table>
69 </md-tab> 69 </md-tab>
  70 + <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
  71 + <tb-extension-table flex
  72 + entity-id="vm.grid.operatingItem().id.id"
  73 + entity-name="vm.grid.operatingItem().name"
  74 + entity-type="{{vm.types.entityType.device}}">
  75 + </tb-extension-table>
  76 + </md-tab>
70 </tb-grid> 77 </tb-grid>
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 --> 17 -->
18 <md-content flex class="md-padding tb-absolute-fill" layout="column"> 18 <md-content flex class="md-padding tb-absolute-fill" layout="column">
19 - <section layout="row" ng-show="!disableAttributeScopeSelection"> 19 + <section ng-show="!disableAttributeScopeSelection">
20 <md-input-container class="md-block" style="width: 200px;"> 20 <md-input-container class="md-block" style="width: 200px;">
21 <label translate>attribute.attributes-scope</label> 21 <label translate>attribute.attributes-scope</label>
22 <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly"> 22 <md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
@@ -26,7 +26,7 @@ @@ -26,7 +26,7 @@
26 </md-select> 26 </md-select>
27 </md-input-container> 27 </md-input-container>
28 </section> 28 </section>
29 - <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}"> 29 + <div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
30 <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default' 30 <md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
31 && !selectedAttributes.length 31 && !selectedAttributes.length
32 && query.search === null"> 32 && query.search === null">
@@ -79,10 +79,8 @@ export default function RelationDialogController($scope, $mdDialog, types, entit @@ -79,10 +79,8 @@ export default function RelationDialogController($scope, $mdDialog, types, entit
79 }); 79 });
80 80
81 function updateEditorSize(element) { 81 function updateEditorSize(element) {
82 - var newWidth = 600;  
83 var newHeight = 200; 82 var newHeight = 200;
84 - angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px")  
85 - .width(newWidth.toString() + "px"); 83 + angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px");
86 vm.editor.resize(); 84 vm.editor.resize();
87 } 85 }
88 86
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 border: 1px solid #C0C0C0; 19 border: 1px solid #C0C0C0;
20 height: 100%; 20 height: 100%;
21 #tb-relation-additional-info { 21 #tb-relation-additional-info {
22 - min-width: 600px; 22 + min-width: 200px;
23 min-height: 200px; 23 min-height: 200px;
24 width: 100%; 24 width: 100%;
25 height: 100%; 25 height: 100%;
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 400px;"> 18 +<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 600px;">
19 <form name="theForm" ng-submit="vm.save()"> 19 <form name="theForm" ng-submit="vm.save()">
20 <md-toolbar> 20 <md-toolbar>
21 <div class="md-toolbar-tools"> 21 <div class="md-toolbar-tools">
  1 +/*
  2 + * Copyright © 2016-2017 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 +
  17 +import beautify from 'js-beautify';
  18 +
  19 +const js_beautify = beautify.js;
  20 +
  21 +/*@ngInject*/
  22 +export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
  23 +
  24 + var vm = this;
  25 +
  26 + vm.types = types;
  27 + vm.isAdd = isAdd;
  28 + vm.entityType = entityType;
  29 + vm.entityId = entityId;
  30 + vm.allExtensions = allExtensions;
  31 +
  32 +
  33 + if (extension) {
  34 + vm.extension = angular.copy(extension);
  35 + editTransformers(vm.extension);
  36 + } else {
  37 + vm.extension = {};
  38 + }
  39 +
  40 +
  41 + vm.extensionTypeChange = function () {
  42 +
  43 + if (vm.extension.type === "HTTP") {
  44 + vm.extension.configuration = {
  45 + "converterConfigurations": []
  46 + };
  47 + }
  48 + if (vm.extension.type === "MQTT") {
  49 + vm.extension.configuration = {
  50 + "brokers": []
  51 + };
  52 + }
  53 + if (vm.extension.type === "OPC UA") {
  54 + vm.extension.configuration = {
  55 + "servers": []
  56 + };
  57 + }
  58 + };
  59 +
  60 + vm.cancel = cancel;
  61 + function cancel() {
  62 + $mdDialog.cancel();
  63 + }
  64 +
  65 + vm.save = save;
  66 + function save() {
  67 + let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
  68 +
  69 + if ($errorElement.length) {
  70 +
  71 + let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
  72 + let $mdDialogTop = angular.element('md-dialog-content').offset().top;
  73 + let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
  74 +
  75 +
  76 + if ($errorElementTop !== $mdDialogTop) {
  77 + angular.element('md-dialog-content').animate({
  78 + scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50
  79 + }, 500);
  80 + $errorElement.eq(0).focus();
  81 + }
  82 + } else {
  83 +
  84 + if(vm.isAdd) {
  85 + vm.allExtensions.push(vm.extension);
  86 + } else {
  87 + var index = vm.allExtensions.indexOf(extension);
  88 + if(index > -1) {
  89 + vm.allExtensions[index] = vm.extension;
  90 + }
  91 + }
  92 +
  93 + $mdDialog.hide();
  94 + saveTransformers();
  95 +
  96 + var editedValue = angular.toJson(vm.allExtensions);
  97 +
  98 + attributeService
  99 + .saveEntityAttributes(
  100 + vm.entityType,
  101 + vm.entityId,
  102 + types.attributesScope.shared.value,
  103 + [{key:"configuration", value:editedValue}]
  104 + )
  105 + .then(function success() {
  106 + });
  107 +
  108 + }
  109 + }
  110 +
  111 + vm.validateId = function() {
  112 + var coincidenceArray = vm.allExtensions.filter(function(ext) {
  113 + return ext.id == vm.extension.id;
  114 + });
  115 + if(coincidenceArray.length) {
  116 + if(!vm.isAdd) {
  117 + if(coincidenceArray[0].id == extension.id) {
  118 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
  119 + } else {
  120 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
  121 + }
  122 + } else {
  123 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
  124 + }
  125 + } else {
  126 + $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
  127 + }
  128 + };
  129 +
  130 + function saveTransformers() {
  131 + if(vm.extension.type == types.extensionType.http) {
  132 + var config = vm.extension.configuration.converterConfigurations;
  133 + if(config && config.length > 0) {
  134 + for(let i=0;i<config.length;i++) {
  135 + for(let j=0;j<config[i].converters.length;j++){
  136 + for(let k=0;k<config[i].converters[j].attributes.length;k++){
  137 + if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
  138 + config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
  139 + }
  140 + delete config[i].converters[j].attributes[k].transformerType;
  141 + }
  142 + for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
  143 + if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
  144 + config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
  145 + }
  146 + delete config[i].converters[j].timeseries[l].transformerType;
  147 + }
  148 + }
  149 + }
  150 + }
  151 + }
  152 + if(vm.extension.type == types.extensionType.mqtt) {
  153 + var brokers = vm.extension.configuration.brokers;
  154 + if(brokers && brokers.length > 0) {
  155 + for(let i=0;i<brokers.length;i++) {
  156 + if(brokers[i].mapping && brokers[i].mapping.length > 0) {
  157 + for(let j=0;j<brokers[i].mapping.length;j++) {
  158 + if(brokers[i].mapping[j].converterType == "json") {
  159 + delete brokers[i].mapping[j].converter.nameExp;
  160 + delete brokers[i].mapping[j].converter.typeExp;
  161 + }
  162 + delete brokers[i].mapping[j].converterType;
  163 + }
  164 + }
  165 + if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
  166 + for(let j=0;j<brokers[i].connectRequests.length;j++) {
  167 + delete brokers[i].connectRequests[j].nameExp;
  168 + }
  169 + }
  170 + if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
  171 + for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
  172 + delete brokers[i].disconnectRequests[j].nameExp;
  173 + }
  174 + }
  175 + if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
  176 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  177 + delete brokers[i].attributeRequests[j].nameExp;
  178 + }
  179 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  180 + delete brokers[i].attributeRequests[j].attrKey;
  181 + }
  182 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  183 + delete brokers[i].attributeRequests[j].requestId;
  184 + }
  185 + }
  186 + }
  187 + }
  188 + }
  189 + }
  190 +
  191 + function editTransformers(extension) {
  192 + if(extension.type == types.extensionType.http) {
  193 + var config = extension.configuration.converterConfigurations;
  194 + for(let i=0;i<config.length;i++) {
  195 + for(let j=0;j<config[i].converters.length;j++){
  196 + for(let k=0;k<config[i].converters[j].attributes.length;k++){
  197 + if(config[i].converters[j].attributes[k].transformer){
  198 + if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
  199 + config[i].converters[j].attributes[k].transformerType = "toDouble";
  200 + } else {
  201 + config[i].converters[j].attributes[k].transformerType = "custom";
  202 + config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
  203 + }
  204 + }
  205 + }
  206 + for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
  207 + if(config[i].converters[j].timeseries[l].transformer){
  208 + if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
  209 + config[i].converters[j].timeseries[l].transformerType = "toDouble";
  210 + } else {
  211 + config[i].converters[j].timeseries[l].transformerType = "custom";
  212 + config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
  213 + }
  214 + }
  215 + }
  216 + }
  217 + }
  218 + }
  219 + if(extension.type == types.extensionType.mqtt) {
  220 + var brokers = extension.configuration.brokers;
  221 + for(let i=0;i<brokers.length;i++) {
  222 + if(brokers[i].mapping && brokers[i].mapping.length > 0) {
  223 + for(let j=0;j<brokers[i].mapping.length;j++) {
  224 + if(brokers[i].mapping[j].converter.type == "json") {
  225 + if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
  226 + brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
  227 + } else {
  228 + brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
  229 + }
  230 + if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
  231 + brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
  232 + } else {
  233 + brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
  234 + }
  235 + brokers[i].mapping[j].converterType = "json";
  236 + } else {
  237 + brokers[i].mapping[j].converterType = "custom";
  238 + }
  239 + }
  240 + }
  241 + if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
  242 + for(let j=0;j<brokers[i].connectRequests.length;j++) {
  243 + if(brokers[i].connectRequests[j].deviceNameTopicExpression) {
  244 + brokers[i].connectRequests[j].nameExp = "deviceNameTopicExpression";
  245 + } else {
  246 + brokers[i].connectRequests[j].nameExp = "deviceNameJsonExpression";
  247 + }
  248 + }
  249 + }
  250 + if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
  251 + for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
  252 + if(brokers[i].disconnectRequests[j].deviceNameTopicExpression) {
  253 + brokers[i].disconnectRequests[j].nameExp = "deviceNameTopicExpression";
  254 + } else {
  255 + brokers[i].disconnectRequests[j].nameExp = "deviceNameJsonExpression";
  256 + }
  257 + }
  258 + }
  259 + if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
  260 + for(let j=0;j<brokers[i].attributeRequests.length;j++) {
  261 + if(brokers[i].attributeRequests[j].deviceNameTopicExpression) {
  262 + brokers[i].attributeRequests[j].nameExp = "deviceNameTopicExpression";
  263 + } else {
  264 + brokers[i].attributeRequests[j].nameExp = "deviceNameJsonExpression";
  265 + }
  266 + if(brokers[i].attributeRequests[j].attributeKeyTopicExpression) {
  267 + brokers[i].attributeRequests[j].attrKey = "attributeKeyTopicExpression";
  268 + } else {
  269 + brokers[i].attributeRequests[j].attrKey = "attributeKeyJsonExpression";
  270 + }
  271 + if(brokers[i].attributeRequests[j].requestIdTopicExpression) {
  272 + brokers[i].attributeRequests[j].requestId = "requestIdTopicExpression";
  273 + } else {
  274 + brokers[i].attributeRequests[j].requestId = "requestIdJsonExpression";
  275 + }
  276 + }
  277 + }
  278 + }
  279 + }
  280 + }
  281 +}
  282 +
  283 +/*@ngInject*/
  284 +export function ParseToNull() {
  285 + var linker = function (scope, elem, attrs, ngModel) {
  286 + ngModel.$parsers.push(function(value) {
  287 + if(value === "") {
  288 + return null;
  289 + }
  290 + return value;
  291 + })
  292 + };
  293 + return {
  294 + restrict: "A",
  295 + link: linker,
  296 + require: "ngModel"
  297 + }
  298 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
  19 + <form name="theForm" ng-submit="vm.save()" novalidate>
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 +
  30 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
  31 +
  32 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  33 +
  34 + <md-dialog-content>
  35 + <div class="md-dialog-content">
  36 + <md-content class="md-padding" layout="column">
  37 + <fieldset ng-disabled="loading">
  38 + <section flex layout="row">
  39 + <md-input-container flex="60" class="md-block" md-is-error="theForm.extensionId.$touched && theForm.extensionId.$invalid">
  40 + <label translate>extension.extension-id</label>
  41 + <input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
  42 + <div ng-messages="theForm.extensionId.$error">
  43 + <div translate ng-message="required">extension.field-required</div>
  44 + <div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
  45 + </div>
  46 + </md-input-container>
  47 +
  48 + <md-input-container flex="40" class="md-block" md-is-error="theForm.extensionType.$touched && theForm.extensionType.$invalid">
  49 + <label translate>extension.extension-type</label>
  50 +
  51 + <md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
  52 + <md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
  53 + {{value}}
  54 + </md-option>
  55 + </md-select>
  56 +
  57 + <div ng-messages="theForm.extensionType.$error">
  58 + <div translate ng-message="required">extension.field-required</div>
  59 + </div>
  60 + </md-input-container>
  61 + </section>
  62 + <div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
  63 + <div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div>
  64 + <div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
  65 + </fieldset>
  66 + </md-content>
  67 + </div>
  68 + </md-dialog-content>
  69 +
  70 + <md-dialog-actions layout="row">
  71 + <md-button type="submit"
  72 + class="md-raised md-primary"
  73 + >
  74 + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
  75 + </md-button>
  76 +
  77 + <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
  78 + </md-button>
  79 + </md-dialog-actions>
  80 + </form>
  81 +</md-dialog>
  1 +/*
  2 + * Copyright © 2016-2017 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 +
  17 +import 'angular-material-data-table/dist/md-data-table.min.css';
  18 +import './extension-table.scss';
  19 +
  20 +/* eslint-disable import/no-unresolved, import/default */
  21 +
  22 +import extensionTableTemplate from './extension-table.tpl.html';
  23 +import extensionDialogTemplate from './extension-dialog.tpl.html';
  24 +
  25 +/* eslint-enable import/no-unresolved, import/default */
  26 +
  27 +import ExtensionDialogController from './extension-dialog.controller'
  28 +import $ from 'jquery';
  29 +
  30 +/*@ngInject*/
  31 +export default function ExtensionTableDirective() {
  32 + return {
  33 + restrict: "E",
  34 + scope: true,
  35 + bindToController: {
  36 + entityId: '=',
  37 + entityType: '@',
  38 + inWidget: '@?',
  39 + ctx: '=?',
  40 + entityName: '='
  41 + },
  42 + controller: ExtensionTableController,
  43 + controllerAs: 'vm',
  44 + templateUrl: extensionTableTemplate
  45 + };
  46 +}
  47 +
  48 +/*@ngInject*/
  49 +function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
  50 +
  51 + let vm = this;
  52 +
  53 + vm.extensions = [];
  54 + vm.allExtensions = [];
  55 + vm.selectedExtensions = [];
  56 + vm.extensionsCount = 0;
  57 +
  58 + vm.query = {
  59 + order: 'id',
  60 + limit: 5,
  61 + page: 1,
  62 + search: null
  63 + };
  64 +
  65 + vm.enterFilterMode = enterFilterMode;
  66 + vm.exitFilterMode = exitFilterMode;
  67 + vm.onReorder = onReorder;
  68 + vm.onPaginate = onPaginate;
  69 + vm.addExtension = addExtension;
  70 + vm.editExtension = editExtension;
  71 + vm.deleteExtension = deleteExtension;
  72 + vm.deleteExtensions = deleteExtensions;
  73 + vm.reloadExtensions = reloadExtensions;
  74 + vm.updateExtensions = updateExtensions;
  75 +
  76 + $scope.$watch("vm.entityId", function(newVal) {
  77 + if (newVal) {
  78 + if ($scope.subscriber) {
  79 + telemetryWebsocketService.unsubscribe($scope.subscriber);
  80 + $scope.subscriber = null;
  81 + }
  82 +
  83 + vm.subscribed = false;
  84 + vm.syncLastTime = $translate.instant('extension.sync.not-available');
  85 +
  86 + subscribeForClientAttributes();
  87 +
  88 + reloadExtensions();
  89 + }
  90 + });
  91 +
  92 + $scope.$on('$destroy', function() {
  93 + if ($scope.subscriber) {
  94 + telemetryWebsocketService.unsubscribe($scope.subscriber);
  95 + $scope.subscriber = null;
  96 + }
  97 + });
  98 +
  99 + $scope.$watch("vm.query.search", function(newVal, prevVal) {
  100 + if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
  101 + updateExtensions();
  102 + }
  103 + });
  104 +
  105 + $scope.$watch('vm.selectedExtensions.length', function (newLength) {
  106 + var selectionMode = newLength ? true : false;
  107 + if (vm.ctx) {
  108 + if (selectionMode) {
  109 + vm.ctx.hideTitlePanel = true;
  110 + $scope.$emit("selectedExtensions", true);
  111 + } else if (vm.query.search == null) {
  112 + vm.ctx.hideTitlePanel = false;
  113 + $scope.$emit("selectedExtensions", false);
  114 + }
  115 + }
  116 + });
  117 +
  118 + $scope.$on("showSearch", function($event, source) {
  119 + if(source.entityId == vm.entityId) {
  120 + enterFilterMode();
  121 + $scope.$emit("filterMode", true);
  122 + }
  123 + });
  124 + $scope.$on("refreshExtensions", function($event, source) {
  125 + if(source.entityId == vm.entityId) {
  126 + reloadExtensions();
  127 + }
  128 + });
  129 + $scope.$on("addExtension", function($event, source) {
  130 + if(source.entityId == vm.entityId) {
  131 + addExtension();
  132 + }
  133 + });
  134 + $scope.$on("exportExtensions", function($event, source) {
  135 + if(source.entityId == vm.entityId) {
  136 + vm.exportExtensions(source.entityName);
  137 + }
  138 + });
  139 + $scope.$on("importExtensions", function($event, source) {
  140 + if(source.entityId == vm.entityId) {
  141 + vm.importExtensions();
  142 + }
  143 + });
  144 +
  145 + function enterFilterMode() {
  146 + vm.query.search = '';
  147 + if(vm.inWidget) {
  148 + vm.ctx.hideTitlePanel = true;
  149 + }
  150 + }
  151 +
  152 + function exitFilterMode() {
  153 + vm.query.search = null;
  154 + updateExtensions();
  155 + if(vm.inWidget) {
  156 + vm.ctx.hideTitlePanel = false;
  157 + $scope.$emit("filterMode", false);
  158 + }
  159 + }
  160 +
  161 + function onReorder() {
  162 + updateExtensions();
  163 + }
  164 +
  165 + function onPaginate() {
  166 + updateExtensions();
  167 + }
  168 +
  169 + function addExtension($event) {
  170 + if ($event) {
  171 + $event.stopPropagation();
  172 + }
  173 + openExtensionDialog($event);
  174 + }
  175 +
  176 + function editExtension($event, extension) {
  177 + if ($event) {
  178 + $event.stopPropagation();
  179 + }
  180 + openExtensionDialog($event, extension);
  181 + }
  182 +
  183 + function openExtensionDialog($event, extension) {
  184 + if ($event) {
  185 + $event.stopPropagation();
  186 + }
  187 + var isAdd = false;
  188 + if(!extension) {
  189 + isAdd = true;
  190 + }
  191 + $mdDialog.show({
  192 + controller: ExtensionDialogController,
  193 + controllerAs: 'vm',
  194 + templateUrl: extensionDialogTemplate,
  195 + parent: angular.element($document[0].body),
  196 + locals: {
  197 + isAdd: isAdd,
  198 + allExtensions: vm.allExtensions,
  199 + entityId: vm.entityId,
  200 + entityType: vm.entityType,
  201 + extension: extension
  202 + },
  203 + bindToController: true,
  204 + targetEvent: $event,
  205 + fullscreen: true,
  206 + skipHide: true
  207 + }).then(function() {
  208 + reloadExtensions();
  209 + }, function () {
  210 + });
  211 + }
  212 +
  213 + function deleteExtension($event, extension) {
  214 + if ($event) {
  215 + $event.stopPropagation();
  216 + }
  217 + if(extension) {
  218 + var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
  219 + var content = $translate.instant('extension.delete-extension-text');
  220 +
  221 + var confirm = $mdDialog.confirm()
  222 + .targetEvent($event)
  223 + .title(title)
  224 + .htmlContent(content)
  225 + .ariaLabel(title)
  226 + .cancel($translate.instant('action.no'))
  227 + .ok($translate.instant('action.yes'));
  228 + $mdDialog.show(confirm).then(function() {
  229 + var editedExtensions = vm.allExtensions.filter(function(ext) {
  230 + return ext.id !== extension.id;
  231 + });
  232 + var editedValue = angular.toJson(editedExtensions);
  233 + attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
  234 + function success() {
  235 + reloadExtensions();
  236 + }
  237 + );
  238 + });
  239 + }
  240 + }
  241 +
  242 + function deleteExtensions($event) {
  243 + if ($event) {
  244 + $event.stopPropagation();
  245 + }
  246 + if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
  247 + var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
  248 + var content = $translate.instant('extension.delete-extensions-text');
  249 +
  250 + var confirm = $mdDialog.confirm()
  251 + .targetEvent($event)
  252 + .title(title)
  253 + .htmlContent(content)
  254 + .ariaLabel(title)
  255 + .cancel($translate.instant('action.no'))
  256 + .ok($translate.instant('action.yes'));
  257 + $mdDialog.show(confirm).then(function () {
  258 + var editedExtensions = angular.copy(vm.allExtensions);
  259 + for (var i = 0; i < vm.selectedExtensions.length; i++) {
  260 + editedExtensions = editedExtensions.filter(function (ext) {
  261 + return ext.id !== vm.selectedExtensions[i].id;
  262 + });
  263 + }
  264 + var editedValue = angular.toJson(editedExtensions);
  265 + attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
  266 + function success() {
  267 + reloadExtensions();
  268 + }
  269 + );
  270 + });
  271 + }
  272 + }
  273 +
  274 + function reloadExtensions() {
  275 + vm.subscribed = false;
  276 + vm.allExtensions.length = 0;
  277 + vm.extensions.length = 0;
  278 + vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
  279 + vm.extensionsPromise.then(
  280 + function success(data) {
  281 + if (data.length) {
  282 + vm.allExtensions = angular.fromJson(data[0].value);
  283 + } else {
  284 + vm.allExtensions = [];
  285 + }
  286 +
  287 + vm.selectedExtensions = [];
  288 + updateExtensions();
  289 + vm.extensionsPromise = null;
  290 + },
  291 + function fail() {
  292 + vm.extensions = [];
  293 + vm.selectedExtensions = [];
  294 + updateExtensions();
  295 + vm.extensionsPromise = null;
  296 + }
  297 + );
  298 + }
  299 +
  300 + function updateExtensions() {
  301 + vm.selectedExtensions = [];
  302 + var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
  303 + if (vm.query.search != null) {
  304 + result = $filter('filter')(result, function(extension) {
  305 + if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
  306 + return true;
  307 + }
  308 + return false;
  309 + });
  310 + }
  311 + vm.extensionsCount = result.length;
  312 + var startIndex = vm.query.limit * (vm.query.page - 1);
  313 + vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
  314 +
  315 + vm.extensionsJSON = angular.toJson(vm.extensions);
  316 + checkForSync();
  317 + }
  318 +
  319 + function subscribeForClientAttributes() {
  320 + if (!vm.subscribed) {
  321 + if (vm.entityId && vm.entityType) {
  322 + $scope.subscriber = {
  323 + subscriptionCommands: [{
  324 + entityType: vm.entityType,
  325 + entityId: vm.entityId,
  326 + scope: 'CLIENT_SCOPE'
  327 + }],
  328 + type: 'attribute',
  329 + onData: function (data) {
  330 + if (data.data) {
  331 + onSubscriptionData(data.data);
  332 + }
  333 + vm.subscribed = true;
  334 + }
  335 + };
  336 + telemetryWebsocketService.subscribe($scope.subscriber);
  337 + }
  338 + }
  339 + }
  340 + function onSubscriptionData(data) {
  341 +
  342 + if ($.isEmptyObject(data)) {
  343 + vm.appliedConfiguration = undefined;
  344 + } else {
  345 + if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) {
  346 + vm.appliedConfiguration = data.appliedConfiguration[0][1];
  347 + }
  348 + }
  349 +
  350 + updateExtensions();
  351 + $scope.$digest();
  352 + }
  353 +
  354 +
  355 + function checkForSync() {
  356 + if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) {
  357 + vm.syncStatus = $translate.instant('extension.sync.sync');
  358 + vm.syncLastTime = formatDate();
  359 + $scope.isSync = true;
  360 + } else {
  361 + vm.syncStatus = $translate.instant('extension.sync.not-sync');
  362 +
  363 + $scope.isSync = false;
  364 + }
  365 + }
  366 +
  367 + function formatDate(date) {
  368 + let d;
  369 + if (date) {
  370 + d = date;
  371 + } else {
  372 + d = new Date();
  373 + }
  374 +
  375 + d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds());
  376 + return d;
  377 +
  378 + function addZero(num) {
  379 + if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) {
  380 + num = '0' + num;
  381 + }
  382 + return num;
  383 + }
  384 + }
  385 +
  386 + vm.importExtensions = function($event) {
  387 + importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
  388 + };
  389 + vm.exportExtensions = function(widgetSourceEntityName) {
  390 + if(vm.inWidget) {
  391 + importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json');
  392 + } else {
  393 + importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json');
  394 + }
  395 + };
  396 +
  397 + /*change function for widget implementing, like vm.exportExtensions*/
  398 + vm.exportExtension = function($event, extension) {
  399 + if ($event) {
  400 + $event.stopPropagation();
  401 + }
  402 + importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json');
  403 + };
  404 +}
  1 +/**
  2 + * Copyright © 2016-2017 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 +@import '../../scss/constants';
  17 +
  18 +
  19 +.extension-table {
  20 +
  21 + md-input-container .md-errors-spacer {
  22 + min-height: 0;
  23 + }
  24 +
  25 + /*&.tb-data-table table.md-table tbody tr td.tb-action-cell,
  26 + &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
  27 + width: 114px;
  28 + }*/
  29 + .sync-widget {
  30 + max-height: 90px;
  31 + overflow: hidden;
  32 + }
  33 + .toolbar-widget {
  34 + min-height: 39px;
  35 + max-height: 39px;
  36 + }
  37 +}
  38 +
  39 +.extension__syncStatus--black {
  40 + color: #000000!important;
  41 +}
  42 +.extension__syncStatus--green {
  43 + color: #228634!important;
  44 +}
  45 +.extension__syncStatus--red {
  46 + color: #862222!important;
  47 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +
  19 +<md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
  20 + <div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
  21 + <md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
  22 + && vm.query.search === null">
  23 + <div class="md-toolbar-tools">
  24 + <span translate>{{ 'extension.extensions' }}</span>
  25 + <span flex></span>
  26 +
  27 + <md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
  28 + <md-icon>file_upload</md-icon>
  29 + <md-tooltip md-direction="top">
  30 + {{ 'extension.import-extensions-configuration' | translate }}
  31 + </md-tooltip>
  32 + </md-button>
  33 + <md-button class="md-icon-button" ng-click="vm.exportExtensions()">
  34 + <md-icon>file_download</md-icon>
  35 + <md-tooltip md-direction="top">
  36 + {{ 'extension.export-extensions-configuration' | translate }}
  37 + </md-tooltip>
  38 + </md-button>
  39 + <md-button class="md-icon-button" ng-click="vm.addExtension($event)">
  40 + <md-icon>add</md-icon>
  41 + <md-tooltip md-direction="top">
  42 + {{ 'action.add' | translate }}
  43 + </md-tooltip>
  44 + </md-button>
  45 + <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
  46 + <md-icon>search</md-icon>
  47 + <md-tooltip md-direction="top">
  48 + {{ 'action.search' | translate }}
  49 + </md-tooltip>
  50 + </md-button>
  51 + <md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
  52 + <md-icon>refresh</md-icon>
  53 + <md-tooltip md-direction="top">
  54 + {{ 'action.refresh' | translate }}
  55 + </md-tooltip>
  56 + </md-button>
  57 + </div>
  58 + </md-toolbar>
  59 + <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
  60 + && vm.query.search != null" ng-class="{'toolbar-widget' : vm.inWidget}">
  61 + <div class="md-toolbar-tools">
  62 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  63 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  64 + <md-tooltip md-direction="top">
  65 + {{ 'action.search' | translate }}
  66 + </md-tooltip>
  67 + </md-button>
  68 + <md-input-container flex>
  69 + <label>&nbsp;</label>
  70 + <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
  71 + </md-input-container>
  72 + <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
  73 + <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
  74 + <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
  75 + {{ 'action.close' | translate }}
  76 + </md-tooltip>
  77 + </md-button>
  78 + </div>
  79 + </md-toolbar>
  80 + <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length" ng-class="{'toolbar-widget' : vm.inWidget}">
  81 + <div class="md-toolbar-tools">
  82 + <span translate
  83 + translate-values="{count: vm.selectedExtensions.length}"
  84 + translate-interpolation="messageformat">extension.selected-extensions</span>
  85 + <span flex></span>
  86 + <md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
  87 + <md-icon>delete</md-icon>
  88 + <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
  89 + {{ 'action.delete' | translate }}
  90 + </md-tooltip>
  91 + </md-button>
  92 + </div>
  93 + </md-toolbar>
  94 +
  95 + <div class="md-padding" flex layout="row" ng-class="{'sync-widget' : vm.inWidget}">
  96 + <md-input-container flex="50" class="md-block">
  97 + <label translate>extension.sync.status</label>
  98 + <input ng-model="vm.syncStatus"
  99 + ng-class="{'extension__syncStatus--green':isSync, 'extension__syncStatus--red':!isSync}"
  100 + disabled
  101 + >
  102 + </md-input-container>
  103 +
  104 + <md-input-container flex="50" class="md-block">
  105 + <label translate>extension.sync.last-sync-time</label>
  106 + <input ng-model="vm.syncLastTime"
  107 + class="extension__syncStatus--black"
  108 + disabled
  109 + >
  110 + </md-input-container>
  111 + </div>
  112 +
  113 + <md-table-container flex>
  114 + <table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
  115 + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
  116 + <tr md-row>
  117 + <th md-column md-order-by="id"><span translate>extension.id</span></th>
  118 + <th md-column md-order-by="type"><span translate>extension.type</span></th>
  119 + <th md-column><span>&nbsp</span></th>
  120 + </tr>
  121 + </thead>
  122 + <tbody md-body>
  123 + <tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
  124 + <td md-cell>{{ extension.id }}</td>
  125 + <td md-cell>{{ extension.type }}</td>
  126 + <td md-cell class="tb-action-cell">
  127 +
  128 + <!--<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
  129 + <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
  130 + <md-tooltip md-direction="top">
  131 + {{ 'extension.export-extension' | translate }}
  132 + </md-tooltip>
  133 + </md-button>-->
  134 +
  135 + <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
  136 + <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
  137 + <md-tooltip md-direction="top">
  138 + {{ 'extension.edit' | translate }}
  139 + </md-tooltip>
  140 + </md-button>
  141 + <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)">
  142 + <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
  143 + <md-tooltip md-direction="top">
  144 + {{ 'extension.delete' | translate }}
  145 + </md-tooltip>
  146 + </md-button>
  147 + </td>
  148 + </tr>
  149 + </tbody>
  150 + </table>
  151 + <md-divider ng-if="vm.inWidget"></md-divider>
  152 + </md-table-container>
  153 + <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
  154 + md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
  155 + md-on-paginate="vm.onPaginate" md-page-select>
  156 + </md-table-pagination>
  157 + </div>
  158 +
  159 +</md-content>
  1 +/*
  2 + * Copyright © 2016-2017 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 +
  17 +import 'brace/ext/language_tools';
  18 +import 'brace/mode/json';
  19 +import 'brace/theme/github';
  20 +
  21 +import './extension-form.scss';
  22 +
  23 +/* eslint-disable angular/log */
  24 +
  25 +import extensionFormHttpTemplate from './extension-form-http.tpl.html';
  26 +
  27 +/* eslint-enable import/no-unresolved, import/default */
  28 +
  29 +/*@ngInject*/
  30 +export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
  31 +
  32 + var linker = function(scope, element) {
  33 +
  34 + var template = $templateCache.get(extensionFormHttpTemplate);
  35 + element.html(template);
  36 +
  37 + scope.types = types;
  38 + scope.theForm = scope.$parent.theForm;
  39 +
  40 + scope.extensionCustomTransformerOptions = {
  41 + useWrapMode: false,
  42 + mode: 'json',
  43 + showGutter: true,
  44 + showPrintMargin: true,
  45 + theme: 'github',
  46 + advanced: {
  47 + enableSnippets: true,
  48 + enableBasicAutocompletion: true,
  49 + enableLiveAutocompletion: true
  50 + },
  51 + onLoad: function(_ace) {
  52 + _ace.$blockScrolling = 1;
  53 + }
  54 + };
  55 +
  56 +
  57 + scope.addConverterConfig = function() {
  58 + var newConverterConfig = {converterId:"", converters:[]};
  59 + scope.converterConfigs.push(newConverterConfig);
  60 +
  61 + scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
  62 + scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
  63 + };
  64 +
  65 + scope.removeConverterConfig = function(config) {
  66 + var index = scope.converterConfigs.indexOf(config);
  67 + if (index > -1) {
  68 + scope.converterConfigs.splice(index, 1);
  69 + }
  70 + };
  71 +
  72 + scope.addConverter = function(converters) {
  73 + var newConverter = {
  74 + deviceNameJsonExpression:"",
  75 + deviceTypeJsonExpression:"",
  76 + attributes:[],
  77 + timeseries:[]
  78 + };
  79 + converters.push(newConverter);
  80 + };
  81 +
  82 + scope.removeConverter = function(converter, converters) {
  83 + var index = converters.indexOf(converter);
  84 + if (index > -1) {
  85 + converters.splice(index, 1);
  86 + }
  87 + };
  88 +
  89 + scope.addAttribute = function(attributes) {
  90 + var newAttribute = {type:"", key:"", value:""};
  91 + attributes.push(newAttribute);
  92 + };
  93 +
  94 + scope.removeAttribute = function(attribute, attributes) {
  95 + var index = attributes.indexOf(attribute);
  96 + if (index > -1) {
  97 + attributes.splice(index, 1);
  98 + }
  99 + };
  100 +
  101 +
  102 + if(scope.isAdd) {
  103 + scope.converterConfigs = scope.config.converterConfigurations;
  104 + scope.addConverterConfig();
  105 + } else {
  106 + scope.converterConfigs = scope.config.converterConfigurations;
  107 + }
  108 +
  109 + scope.transformerTypeChange = function(attribute) {
  110 + attribute.transformer = "";
  111 + };
  112 +
  113 + scope.validateTransformer = function (model, editorName) {
  114 + if(model && model.length) {
  115 + try {
  116 + angular.fromJson(model);
  117 + scope.theForm[editorName].$setValidity('transformerJSON', true);
  118 + } catch(e) {
  119 + scope.theForm[editorName].$setValidity('transformerJSON', false);
  120 + }
  121 + }
  122 + };
  123 +
  124 + scope.collapseValidation = function(index, id) {
  125 + var invalidState = angular.element('#'+id+':has(.ng-invalid)');
  126 + if(invalidState.length) {
  127 + invalidState.addClass('inner-invalid');
  128 + }
  129 + };
  130 +
  131 + scope.expandValidation = function (index, id) {
  132 + var invalidState = angular.element('#'+id);
  133 + invalidState.removeClass('inner-invalid');
  134 + };
  135 +
  136 + $compile(element.contents())(scope);
  137 + };
  138 +
  139 + return {
  140 + restrict: "A",
  141 + link: linker,
  142 + scope: {
  143 + config: "=",
  144 + isAdd: "="
  145 + }
  146 + }
  147 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-card class="extension-form extension-http">
  19 + <md-card-title>
  20 + <md-card-title-text>
  21 + <span translate class="md-headline">extension.configuration</span>
  22 + </md-card-title-text>
  23 + </md-card-title>
  24 + <md-card-content>
  25 + <v-accordion id="http-converter-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  26 + <v-pane id="http-converters-pane" expanded="true">
  27 + <v-pane-header>
  28 + {{ 'extension.converter-configurations' | translate }}
  29 + </v-pane-header>
  30 + <v-pane-content>
  31 + <div ng-if="converterConfigs.length > 0">
  32 + <ol class="list-group">
  33 + <li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
  34 + <md-button aria-label="{{ 'action.remove' | translate }}"
  35 + class="md-icon-button"
  36 + ng-click="removeConverterConfig(config)"
  37 + ng-hide="converterConfigs.length < 2"
  38 + >
  39 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  40 + <md-tooltip md-direction="top">
  41 + {{ 'action.remove' | translate }}
  42 + </md-tooltip>
  43 + </md-button>
  44 + <md-card>
  45 + <md-card-content>
  46 +
  47 + <md-input-container class="md-block" md-is-error="theForm['httpConverterId_' + configIndex].$touched && theForm['httpConverterId_' + configIndex].$invalid">
  48 + <label translate>extension.converter-id</label>
  49 + <input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
  50 + <div ng-messages="theForm['httpConverterId_' + configIndex].$error">
  51 + <div translate ng-message="required">extension.field-required</div>
  52 + </div>
  53 + </md-input-container>
  54 + <md-input-container class="md-block">
  55 + <label translate>extension.token</label>
  56 + <input name="httpToken" ng-model="config.token" parse-to-null>
  57 + </md-input-container>
  58 + <v-accordion id="http-converters-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  59 + <v-pane id="http-converters-pane_{{configIndex}}" expanded="true">
  60 + <v-pane-header>
  61 + {{ 'extension.converters' | translate }}
  62 + </v-pane-header>
  63 + <v-pane-content>
  64 + <div ng-if="config.converters.length > 0">
  65 + <ol class="list-group">
  66 + <li class="list-group-item"
  67 + ng-repeat="(converterIndex,converter) in config.converters"
  68 + >
  69 + <md-button aria-label="{{ 'action.remove' | translate }}"
  70 + class="md-icon-button"
  71 + ng-click="removeConverter(converter, config.converters)"
  72 + ng-hide="config.converters.length < 2"
  73 + >
  74 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  75 + <md-tooltip md-direction="top">
  76 + {{ 'action.remove' | translate }}
  77 + </md-tooltip>
  78 + </md-button>
  79 + <md-card>
  80 + <md-card-content>
  81 + <md-input-container class="md-block" md-is-error="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceNameExp_' + configIndex + converterIndex].$invalid">
  82 + <label translate>extension.device-name-expression</label>
  83 + <input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
  84 + <div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
  85 + <div translate ng-message="required">extension.field-required</div>
  86 + </div>
  87 + </md-input-container>
  88 + <md-input-container class="md-block" md-is-error="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$invalid">
  89 + <label translate>extension.device-type-expression</label>
  90 + <input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
  91 + <div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
  92 + <div translate ng-message="required">extension.field-required</div>
  93 + </div>
  94 + </md-input-container>
  95 +
  96 + <v-accordion id="http-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  97 + <v-pane id="http-attributes-pane_{{configIndex}}{{converterIndex}}">
  98 + <v-pane-header>
  99 + {{ 'extension.attributes' | translate }}
  100 + </v-pane-header>
  101 + <v-pane-content>
  102 + <div ng-if="converter.attributes.length > 0">
  103 + <ol class="list-group">
  104 + <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
  105 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
  106 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  107 + <md-tooltip md-direction="top">
  108 + {{ 'action.remove' | translate }}
  109 + </md-tooltip>
  110 + </md-button>
  111 + <md-card>
  112 + <md-card-content>
  113 + <section flex layout="row">
  114 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$invalid">
  115 + <label translate>extension.key</label>
  116 + <input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
  117 + <div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
  118 + <div translate ng-message="required">extension.field-required</div>
  119 + </div>
  120 + </md-input-container>
  121 + <md-input-container flex="40" class="md-block" md-is-error="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$invalid">
  122 + <label translate>extension.type</label>
  123 + <md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
  124 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
  125 + {{attrTypeValue | translate}}
  126 + </md-option>
  127 + </md-select>
  128 + <div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
  129 + <div translate ng-message="required">extension.field-required</div>
  130 + </div>
  131 + </md-input-container>
  132 + </section>
  133 + <section flex layout="row">
  134 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$invalid">
  135 + <label translate>extension.value</label>
  136 + <input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
  137 + <div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
  138 + <div translate ng-message="required">extension.field-required</div>
  139 + </div>
  140 + </md-input-container>
  141 +
  142 +
  143 + <md-input-container flex="40" class="md-block">
  144 + <label translate>extension.transformer</label>
  145 + <md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
  146 + <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
  147 + {{value | translate}}
  148 + </md-option>
  149 + </md-select>
  150 + </md-input-container>
  151 + </section>
  152 +
  153 + <div ng-if='attribute.transformerType == "custom"'>
  154 + <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
  155 + <div flex class="tb-extension-custom-transformer-panel">
  156 + <div flex class="tb-extension-custom-transformer"
  157 + ui-ace="extensionCustomTransformerOptions"
  158 + ng-model="attribute.transformer"
  159 + name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
  160 + ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
  161 + required>
  162 + </div>
  163 + </div>
  164 + <div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
  165 + <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
  166 + <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
  167 + </div>
  168 + </div>
  169 +
  170 +
  171 + </md-card-content>
  172 + </md-card>
  173 + </li>
  174 + </ol>
  175 + </div>
  176 + <div flex layout="row" layout-align="start center">
  177 + <md-button class="md-primary md-raised"
  178 + ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
  179 + <md-icon class="material-icons">add</md-icon>
  180 + <span translate>extension.add-attribute</span>
  181 + </md-button>
  182 + </div>
  183 + </v-pane-content>
  184 + </v-pane>
  185 + </v-accordion>
  186 +
  187 +
  188 + <v-accordion id="http-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
  189 + <v-pane id="http-timeseries-pane_{{configIndex}}{{converterIndex}}">
  190 + <v-pane-header>
  191 + {{ 'extension.timeseries' | translate }}
  192 + </v-pane-header>
  193 + <v-pane-content>
  194 + <div ng-if="converter.timeseries.length > 0">
  195 + <ol class="list-group">
  196 + <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
  197 + <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
  198 + <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
  199 + <md-tooltip md-direction="top">
  200 + {{ 'action.remove' | translate }}
  201 + </md-tooltip>
  202 + </md-button>
  203 + <md-card>
  204 + <md-card-content>
  205 + <section flex layout="row">
  206 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$invalid">
  207 + <label translate>extension.key</label>
  208 + <input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
  209 + <div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
  210 + <div translate ng-message="required">extension.field-required</div>
  211 + </div>
  212 + </md-input-container>
  213 + <md-input-container flex="40" class="md-block" md-is-error="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$invalid">
  214 + <label translate>extension.type</label>
  215 + <md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
  216 + <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
  217 + {{attrTypeValue | translate}}
  218 + </md-option>
  219 + </md-select>
  220 + <div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
  221 + <div translate ng-message="required">extension.field-required</div>
  222 + </div>
  223 + </md-input-container>
  224 + </section>
  225 + <section flex layout="row">
  226 + <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$invalid">
  227 + <label translate>extension.value</label>
  228 + <input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
  229 + <div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
  230 + <div translate ng-message="required">extension.field-required</div>
  231 + </div>
  232 + </md-input-container>
  233 +
  234 +
  235 + <md-input-container flex="40" class="md-block">
  236 + <label translate>extension.transformer</label>
  237 + <md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
  238 + <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
  239 + {{value | translate}}
  240 + </md-option>
  241 + </md-select>
  242 + </md-input-container>
  243 + </section>
  244 +
  245 + <div ng-if='timeseries.transformerType == "custom"'>
  246 + <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
  247 + <div flex class="tb-extension-custom-transformer-panel">
  248 + <div flex class="tb-extension-custom-transformer"
  249 + ui-ace="extensionCustomTransformerOptions"
  250 + ng-model="timeseries.transformer"
  251 + name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
  252 + ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
  253 + required>
  254 + </div>
  255 + </div>
  256 + <div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
  257 + <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
  258 + <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
  259 + </div>
  260 + </div>
  261 +
  262 +
  263 + </md-card-content>
  264 + </md-card>
  265 + </li>
  266 + </ol>
  267 + </div>
  268 + <div flex layout="row" layout-align="start center">
  269 + <md-button class="md-primary md-raised"
  270 + ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
  271 + <md-icon class="material-icons">add</md-icon>
  272 + <span translate>extension.add-timeseries</span>
  273 + </md-button>
  274 + </div>
  275 + </v-pane-content>
  276 + </v-pane>
  277 + </v-accordion>
  278 + </md-card-content>
  279 + </md-card>
  280 + </li>
  281 + </ol>
  282 + </div>
  283 + <div flex layout="row" layout-align="start center">
  284 + <md-button class="md-primary md-raised"
  285 + ng-click="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
  286 + <md-icon class="material-icons">add</md-icon>
  287 + <span translate>extension.add-converter</span>
  288 + </md-button>
  289 + </div>
  290 + </v-pane-content>
  291 + </v-pane>
  292 + </v-accordion>
  293 +
  294 + </md-card-content>
  295 + </md-card>
  296 + </li>
  297 + </ol>
  298 + </div>
  299 + <div flex layout="row" layout-align="start center">
  300 + <md-button class="md-primary md-raised"
  301 + ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
  302 + <md-icon class="material-icons">add</md-icon>
  303 + <span translate>extension.add-config</span>
  304 + </md-button>
  305 + </div>
  306 + </v-pane-content>
  307 + </v-pane>
  308 + </v-accordion>
  309 + <!--{{config}}-->
  310 + </md-card-content>
  311 +</md-card>
  1 +/*
  2 + * Copyright © 2016-2017 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 +
  17 +import './extension-form.scss';
  18 +
  19 +/* eslint-disable angular/log */
  20 +
  21 +import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
  22 +
  23 +/* eslint-enable import/no-unresolved, import/default */
  24 +
  25 +/*@ngInject*/
  26 +export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
  27 +
  28 + var linker = function(scope, element) {
  29 +
  30 + var template = $templateCache.get(extensionFormMqttTemplate);
  31 + element.html(template);
  32 +
  33 + scope.types = types;
  34 + scope.theForm = scope.$parent.theForm;
  35 +
  36 + scope.deviceNameExpressions = {
  37 + deviceNameJsonExpression: "extension.converter-json",
  38 + deviceNameTopicExpression: "extension.topic"
  39 + };
  40 + scope.deviceTypeExpressions = {
  41 + deviceTypeJsonExpression: "extension.converter-json",
  42 + deviceTypeTopicExpression: "extension.topic"
  43 + };
  44 + scope.attributeKeyExpressions = {
  45 + attributeKeyJsonExpression: "extension.converter-json",
  46 + attributeKeyTopicExpression: "extension.topic"
  47 + };
  48 + scope.requestIdExpressions = {
  49 + requestIdJsonExpression: "extension.converter-json",
  50 + requestIdTopicExpression: "extension.topic"
  51 + }
  52 +
  53 + scope.extensionCustomConverterOptions = {
  54 + useWrapMode: false,
  55 + mode: 'json',
  56 + showGutter: true,
  57 + showPrintMargin: true,
  58 + theme: 'github',
  59 + advanced: {
  60 + enableSnippets: true,
  61 + enableBasicAutocompletion: true,
  62 + enableLiveAutocompletion: true
  63 + },
  64 + onLoad: function(_ace) {
  65 + _ace.$blockScrolling = 1;
  66 + }
  67 + };
  68 +
  69 + scope.updateValidity = function () {
  70 + if(scope.brokers.length) {
  71 + for(let i=0;i<scope.brokers.length;i++) {
  72 + if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
  73 + if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
  74 + scope.theForm.$setValidity('cert.PEM', false);
  75 + break;
  76 + } else {
  77 + scope.theForm.$setValidity('cert.PEM', true);
  78 + }
  79 + }
  80 + }
  81 + }
  82 + };
  83 +
  84 + scope.$watch('brokers', function() {
  85 + scope.updateValidity();
  86 + }, true);
  87 +
  88 + scope.addBroker = function() {
  89 + var newBroker = {
  90 + host: "localhost",
  91 + port: 1882,
  92 + ssl: false,
  93 + retryInterval: 3000,
  94 + credentials: {type:"anonymous"},
  95 + mapping: [],
  96 + connectRequests: [],
  97 + disconnectRequests: [],
  98 + attributeRequests: [],
  99 + attributeUpdates: [],
  100 + serverSideRpc: []
  101 + };
  102 + scope.brokers.push(newBroker);
  103 + };
  104 +
  105 + scope.removeBroker = function(broker) {
  106 + var index = scope.brokers.indexOf(broker);
  107 + if (index > -1) {
  108 + scope.brokers.splice(index, 1);
  109 + }
  110 + };
  111 +
  112 + if(scope.isAdd) {
  113 + scope.brokers = [];
  114 + scope.config.brokers = scope.brokers;
  115 + scope.addBroker();
  116 + } else {
  117 + scope.brokers = scope.config.brokers;
  118 + }
  119 +
  120 + scope.addMap = function(mapping) {
  121 + var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
  122 +
  123 + mapping.push(newMap);
  124 + };
  125 +
  126 + scope.removeMap = function(map, mapping) {
  127 + var index = mapping.indexOf(map);
  128 + if (index > -1) {
  129 + mapping.splice(index, 1);
  130 + }
  131 + };
  132 +
  133 + scope.addAttribute = function(attributes) {
  134 + var newAttribute = {type:"", key:"", value:""};
  135 + attributes.push(newAttribute);
  136 + };
  137 +
  138 + scope.removeAttribute = function(attribute, attributes) {
  139 + var index = attributes.indexOf(attribute);
  140 + if (index > -1) {
  141 + attributes.splice(index, 1);
  142 + }
  143 + };
  144 +
  145 + scope.addConnectRequest = function(requests, type) {
  146 + var newRequest = {};
  147 + if(type == "connect") {
  148 + newRequest.topicFilter = "sensors/connect";
  149 + } else {
  150 + newRequest.topicFilter = "sensors/disconnect";
  151 + }
  152 + requests.push(newRequest);
  153 + };
  154 +
  155 + scope.addAttributeRequest = function(requests) {
  156 + var newRequest = {
  157 + topicFilter: "sensors/attributes",
  158 + clientScope: false,
  159 + responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}",
  160 + valueExpression: "${attributeValue}"
  161 + };
  162 + requests.push(newRequest);
  163 + };
  164 +
  165 + scope.addAttributeUpdate = function(updates) {
  166 + var newUpdate = {
  167 + deviceNameFilter: ".*",
  168 + attributeFilter: ".*",
  169 + topicExpression: "sensor/${deviceName}/${attributeKey}",
  170 + valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}"
  171 + }
  172 + updates.push(newUpdate);
  173 + };
  174 +
  175 + scope.addServerSideRpc = function(rpcRequests) {
  176 + var newRpc = {
  177 + deviceNameFilter: ".*",
  178 + methodFilter: "echo",
  179 + requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}",
  180 + responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}",
  181 + responseTimeout: 10000,
  182 + valueExpression: "${params}"
  183 + };
  184 + rpcRequests.push(newRpc);
  185 + };
  186 +
  187 + scope.changeCredentials = function(broker) {
  188 + var type = broker.credentials.type;
  189 + broker.credentials = {};
  190 + broker.credentials.type = type;
  191 + };
  192 +
  193 + scope.changeConverterType = function(map) {
  194 + if(map.converterType == "custom"){
  195 + map.converter = "";
  196 + }
  197 + if(map.converterType == "json") {
  198 + map.converter = {attributes:[],timeseries:[]};
  199 + }
  200 + };
  201 +
  202 + scope.changeNameExpression = function(element, type) {
  203 + if(element.nameExp == "deviceNameJsonExpression") {
  204 + if(element.deviceNameTopicExpression) {
  205 + delete element.deviceNameTopicExpression;
  206 + }
  207 + if(type) {
  208 + element.deviceNameJsonExpression = "${$.serialNumber}";
  209 + }
  210 + }
  211 + if(element.nameExp == "deviceNameTopicExpression") {
  212 + if(element.deviceNameJsonExpression) {
  213 + delete element.deviceNameJsonExpression;
  214 + }
  215 + if(type && type == "connect") {
  216 + element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)";
  217 + }
  218 + if(type && type == "disconnect") {
  219 + element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)";
  220 + }
  221 + if(type && type == "attribute") {
  222 + element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)";
  223 + }
  224 + }
  225 + };
  226 +
  227 + scope.changeTypeExpression = function(converter) {
  228 + if(converter.typeExp == "deviceTypeJsonExpression") {
  229 + if(converter.deviceTypeTopicExpression) {
  230 + delete converter.deviceTypeTopicExpression;
  231 + }
  232 + }
  233 + if(converter.typeExp == "deviceTypeTopicExpression") {
  234 + if(converter.deviceTypeJsonExpression) {
  235 + delete converter.deviceTypeJsonExpression;
  236 + }
  237 + }
  238 + };
  239 +
  240 + scope.changeAttrKeyExpression = function(request) {
  241 + if(request.attrKey == "attributeKeyJsonExpression") {
  242 + if(request.attributeKeyTopicExpression) {
  243 + delete request.attributeKeyTopicExpression;
  244 + }
  245 + request.attributeKeyJsonExpression = "${$.key}";
  246 + }
  247 + if(request.attrKey == "attributeKeyTopicExpression") {
  248 + if(request.attributeKeyJsonExpression) {
  249 + delete request.attributeKeyJsonExpression;
  250 + }
  251 + request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)";
  252 + }
  253 + };
  254 +
  255 + scope.changeRequestIdExpression = function(request) {
  256 + if(request.requestId == "requestIdJsonExpression") {
  257 + if(request.requestIdTopicExpression) {
  258 + delete request.requestIdTopicExpression;
  259 + }
  260 + request.requestIdJsonExpression = "${$.requestId}";
  261 + }
  262 + if(request.requestId == "requestIdTopicExpression") {
  263 + if(request.requestIdJsonExpression) {
  264 + delete request.requestIdJsonExpression;
  265 + }
  266 + request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)";
  267 + }
  268 + };
  269 +
  270 + scope.validateCustomConverter = function(model, editorName) {
  271 + if(model && model.length) {
  272 + try {
  273 + angular.fromJson(model);
  274 + scope.theForm[editorName].$setValidity('converterJSON', true);
  275 + } catch(e) {
  276 + scope.theForm[editorName].$setValidity('converterJSON', false);
  277 + }
  278 + }
  279 + };
  280 +
  281 + scope.fileAdded = function($file, broker, fileType) {
  282 + var reader = new FileReader();
  283 + reader.onload = function(event) {
  284 + scope.$apply(function() {
  285 + if(event.target.result) {
  286 + scope.theForm.$setDirty();
  287 + var addedFile = event.target.result;
  288 + if (addedFile && addedFile.length > 0) {
  289 + if(fileType == "caCert") {
  290 + broker.credentials.caCertFileName = $file.name;
  291 + broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
  292 + }
  293 + if(fileType == "privateKey") {
  294 + broker.credentials.privateKeyFileName = $file.name;
  295 + broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
  296 + }
  297 + if(fileType == "Cert") {
  298 + broker.credentials.certFileName = $file.name;
  299 + broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
  300 + }
  301 + }
  302 + }
  303 + });
  304 + };
  305 + reader.readAsDataURL($file.file);
  306 + };
  307 +
  308 + scope.clearFile = function(broker, fileType) {
  309 + scope.theForm.$setDirty();
  310 + if(fileType == "caCert") {
  311 + broker.credentials.caCertFileName = null;
  312 + broker.credentials.caCert = null;
  313 + }
  314 + if(fileType == "privateKey") {
  315 + broker.credentials.privateKeyFileName = null;
  316 + broker.credentials.privateKey = null;
  317 + }
  318 + if(fileType == "Cert") {
  319 + broker.credentials.certFileName = null;
  320 + broker.credentials.cert = null;
  321 + }
  322 + };
  323 +
  324 + scope.collapseValidation = function(index, id) {
  325 + var invalidState = angular.element('#'+id+':has(.ng-invalid)');
  326 + if(invalidState.length) {
  327 + invalidState.addClass('inner-invalid');
  328 + }
  329 + };
  330 +
  331 + scope.expandValidation = function (index, id) {
  332 + var invalidState = angular.element('#'+id);
  333 + invalidState.removeClass('inner-invalid');
  334 + };
  335 +
  336 + $compile(element.contents())(scope);
  337 + };
  338 +
  339 + return {
  340 + restrict: "A",
  341 + link: linker,
  342 + scope: {
  343 + config: "=",
  344 + isAdd: "="
  345 + }
  346 + }
  347 +}