thingsboard-aplcache
Changes
application/pom.xml 10(+10 -0)
application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java 2(+1 -1)
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java 6(+6 -0)
common/data/pom.xml 2(+1 -1)
common/message/pom.xml 2(+1 -1)
common/pom.xml 2(+1 -1)
common/transport/pom.xml 2(+1 -1)
dao/pom.xml 2(+1 -1)
dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java 1(+1 -0)
extensions/extension-kafka/pom.xml 2(+1 -1)
extensions/extension-mqtt/pom.xml 2(+1 -1)
extensions/extension-sns/pom.xml 81(+81 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java 31(+31 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java 37(+37 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java 45(+45 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java 30(+30 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java 63(+63 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java 79(+79 -0)
extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java 30(+30 -0)
extensions/extension-sqs/pom.xml 81(+81 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java 31(+31 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java 39(+39 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java 49(+49 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java 30(+30 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java 31(+31 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java 39(+39 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java 46(+46 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java 32(+32 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java 84(+84 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java 78(+78 -0)
extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java 30(+30 -0)
extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java 69(+69 -0)
extensions/pom.xml 4(+3 -1)
extensions-api/pom.xml 2(+1 -1)
extensions-core/pom.xml 2(+1 -1)
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java 8(+6 -2)
pom.xml 12(+12 -0)
resume.bat 18(+18 -0)
tools/pom.xml 2(+1 -1)
tools/src/main/python/mqtt-send-telemetry.py 36(+36 -0)
transport/coap/pom.xml 2(+1 -1)
transport/http/pom.xml 2(+1 -1)
transport/mqtt/pom.xml 2(+1 -1)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java 2(+2 -0)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java 44(+25 -19)
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java 27(+17 -10)
transport/pom.xml 2(+1 -1)
ui/package.json 1(+1 -0)
ui/pom.xml 2(+1 -1)
ui/src/app/api/login.service.js 16(+8 -8)
ui/src/app/api/user.service.js 19(+10 -9)
ui/src/app/api/widget.service.js 3(+2 -1)
ui/src/app/app.js 3(+3 -0)
ui/src/app/asset/asset-card.tpl.html 2(+1 -1)
ui/src/app/common/dashboard-utils.service.js 18(+16 -2)
ui/src/app/common/types.constant.js 47(+47 -0)
ui/src/app/components/dashboard.directive.js 177(+88 -89)
ui/src/app/components/js-func.scss 2(+1 -1)
ui/src/app/device/devices.tpl.html 7(+7 -0)
ui/src/app/extension/extension-dialog.controller.js 298(+298 -0)
ui/src/app/extension/extension-table.directive.js 404(+404 -0)
ui/src/app/extension/extension-table.scss 47(+47 -0)
ui/src/app/extension/extension-table.tpl.html 159(+159 -0)
ui/src/app/extension/index.js 29(+29 -0)
ui/src/app/home/home-links.controller.js 38(+37 -1)
ui/src/app/home/home-links.scss 26(+26 -0)
ui/src/app/home/home-links.tpl.html 16(+8 -8)
ui/src/app/layout/index.js 2(+2 -0)
ui/src/app/locale/locale.constant.js 131(+129 -2)
ui/src/app/widget/lib/CanvasDigitalGauge.js 18(+16 -2)
ui/src/app/widget/lib/extensions-table-widget.js 135(+135 -0)
ui/src/index.html 2(+1 -1)
ui/src/scss/animations.scss 2(+1 -1)
ui/src/scss/main.scss 6(+5 -1)
Details
application/pom.xml 10(+10 -0)
diff --git a/application/pom.xml b/application/pom.xml
index 890da91..c7d3240 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -496,6 +496,16 @@
<artifactId>extension-mqtt</artifactId>
<classifier>extension</classifier>
</artifactItem>
+ <artifactItem>
+ <groupId>org.thingsboard.extensions</groupId>
+ <artifactId>extension-sqs</artifactId>
+ <classifier>extension</classifier>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.thingsboard.extensions</groupId>
+ <artifactId>extension-sns</artifactId>
+ <classifier>extension</classifier>
+ </artifactItem>
</artifactItems>
</configuration>
</execution>
diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
index 164366d..d811393 100644
--- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json
@@ -18,7 +18,7 @@
"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",
"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}",
"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}",
- "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}"
+ "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}"
}
}
]
diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 034209e..0fc0085 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -34,7 +34,7 @@
"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",
"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}",
"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}",
- "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;\"}]}]}"
+ "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;\"}]}]}"
}
},
{
diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
new file mode 100644
index 0000000..c963835
--- /dev/null
+++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json
@@ -0,0 +1,25 @@
+{
+ "widgetsBundle": {
+ "alias": "gateway_widgets",
+ "title": "Gateway widgets",
+ "image": null
+ },
+ "widgetTypes": [
+ {
+ "alias": "extension_configuration_widget",
+ "name": "Extensions table",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 9,
+ "sizeY": 6.5,
+ "resources": [],
+ "templateHtml": "<tb-extensions-table-widget \n ctx=\"ctx\">\n</tb-extensions-table-widget>",
+ "templateCss": "#container {\n overflow: auto;\n}",
+ "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",
+ "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}",
+ "dataKeySettingsSchema": "{}\n",
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"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\":{}}"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
index c695ec8..7a7e3ff 100644
--- a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -170,7 +170,7 @@ class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
if (ruleToPluginMsgOptional.isPresent()) {
RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
- logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg);
+ logger.debug("[{}] Device msg is converted to: {}", entityId, ruleToPluginMsg);
context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
if (action.isOneWayAction()) {
pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);
diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
index 6395798..6755408 100644
--- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
+++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
@@ -28,6 +28,7 @@ import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -148,6 +149,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
}
@Override
+ public void configure(WebSecurity web) throws Exception {
+ web.ignoring().antMatchers("/static/**");
+ }
+
+ @Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl().and().frameOptions().disable()
.and()
diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
index 0246e07..cb2f5b9 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java
@@ -19,8 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
@@ -30,7 +28,6 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.UserCredentials;
-import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.exception.ThingsboardErrorCode;
import org.thingsboard.server.exception.ThingsboardException;
import org.thingsboard.server.service.mail.MailService;
@@ -78,9 +75,10 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void changePassword (
- @RequestParam(value = "currentPassword") String currentPassword,
- @RequestParam(value = "newPassword") String newPassword) throws ThingsboardException {
+ @RequestBody JsonNode changePasswordRequest) throws ThingsboardException {
try {
+ String currentPassword = changePasswordRequest.get("currentPassword").asText();
+ String newPassword = changePasswordRequest.get("newPassword").asText();
SecurityUser securityUser = getCurrentUser();
UserCredentials userCredentials = userService.findUserCredentialsByUserId(securityUser.getId());
if (!passwordEncoder.matches(currentPassword, userCredentials.getPassword())) {
@@ -118,9 +116,10 @@ public class AuthController extends BaseController {
@RequestMapping(value = "/noauth/resetPasswordByEmail", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void requestResetPasswordByEmail (
- @RequestParam(value = "email") String email,
+ @RequestBody JsonNode resetPasswordByEmailRequest,
HttpServletRequest request) throws ThingsboardException {
try {
+ String email = resetPasswordByEmailRequest.get("email").asText();
UserCredentials userCredentials = userService.requestPasswordReset(email);
String baseUrl = constructBaseUrl(request);
String resetUrl = String.format("%s/api/noauth/resetPassword?resetToken=%s", baseUrl,
@@ -158,10 +157,11 @@ public class AuthController extends BaseController {
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JsonNode activateUser(
- @RequestParam(value = "activateToken") String activateToken,
- @RequestParam(value = "password") String password,
+ @RequestBody JsonNode activateRequest,
HttpServletRequest request) throws ThingsboardException {
try {
+ String activateToken = activateRequest.get("activateToken").asText();
+ String password = activateRequest.get("password").asText();
String encodedPassword = passwordEncoder.encode(password);
UserCredentials credentials = userService.activateUserCredentials(activateToken, encodedPassword);
User user = userService.findUserById(credentials.getUserId());
@@ -194,10 +194,11 @@ public class AuthController extends BaseController {
@ResponseStatus(value = HttpStatus.OK)
@ResponseBody
public JsonNode resetPassword(
- @RequestParam(value = "resetToken") String resetToken,
- @RequestParam(value = "password") String password,
+ @RequestBody JsonNode resetPasswordRequest,
HttpServletRequest request) throws ThingsboardException {
try {
+ String resetToken = resetPasswordRequest.get("resetToken").asText();
+ String password = resetPasswordRequest.get("password").asText();
UserCredentials userCredentials = userService.findUserCredentialsByResetToken(resetToken);
if (userCredentials != null) {
String encodedPassword = passwordEncoder.encode(password);
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
index 689f316..19e4329 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -221,7 +221,10 @@ public abstract class AbstractControllerTest {
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
- JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class);
+ JsonNode activateRequest = new ObjectMapper().createObjectNode()
+ .put("activateToken", TestMailService.currentActivateToken)
+ .put("password", password);
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, user.getEmail());
return savedUser;
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
index 8dac038..e3c87f6 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
@@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Assert;
import org.junit.Test;
@@ -73,7 +74,11 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
- JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class);
+ JsonNode activateRequest = new ObjectMapper().createObjectNode()
+ .put("activateToken", TestMailService.currentActivateToken)
+ .put("password", "testPassword");
+
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user")
@@ -117,13 +122,21 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest {
User savedUser = createUserAndLogin(user, "testPassword1");
logout();
- doPost("/api/noauth/resetPasswordByEmail", "email", email)
+
+ JsonNode resetPasswordByEmailRequest = new ObjectMapper().createObjectNode()
+ .put("email", email);
+
+ doPost("/api/noauth/resetPasswordByEmail", resetPasswordByEmailRequest)
.andExpect(status().isOk());
doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
-
- JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class);
+
+ JsonNode resetPasswordRequest = new ObjectMapper().createObjectNode()
+ .put("resetToken", TestMailService.currentResetPasswordToken)
+ .put("password", "testPassword2");
+
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", resetPasswordRequest).andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenInfo, email);
doGet("/api/auth/user")
common/data/pom.xml 2(+1 -1)
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 2c9603b..11e62e8 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Common Data</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
common/message/pom.xml 2(+1 -1)
diff --git a/common/message/pom.xml b/common/message/pom.xml
index 3f36cdf..33466a6 100644
--- a/common/message/pom.xml
+++ b/common/message/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Common Messages</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
common/pom.xml 2(+1 -1)
diff --git a/common/pom.xml b/common/pom.xml
index 0320a4a..dfba528 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -28,7 +28,7 @@
<packaging>pom</packaging>
<name>Thingsboard Server Commons</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
common/transport/pom.xml 2(+1 -1)
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index f609e9a..884f455 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Common Transport components</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
dao/pom.xml 2(+1 -1)
diff --git a/dao/pom.xml b/dao/pom.xml
index 9805b6b..d9463e4 100644
--- a/dao/pom.xml
+++ b/dao/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server DAO Layer</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
index 3574e83..86afdb4 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
@@ -384,7 +384,7 @@ public class BaseRelationService implements RelationService {
Set<EntityRelation> children = new HashSet<>(findRelations(rootId, direction).get());
Set<EntityId> childrenIds = new HashSet<>();
for (EntityRelation childRelation : children) {
- log.info("Found Relation: {}", childRelation);
+ log.trace("Found Relation: {}", childRelation);
EntityId childId;
if (direction == EntitySearchDirection.FROM) {
childId = childRelation.getTo();
@@ -392,9 +392,9 @@ public class BaseRelationService implements RelationService {
childId = childRelation.getFrom();
}
if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
- log.info("Adding Relation: {}", childId);
+ log.trace("Adding Relation: {}", childId);
if (childrenIds.add(childId)) {
- log.info("Added Relation: {}", childId);
+ log.trace("Added Relation: {}", childId);
}
}
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
index 7c34aa1..7828267 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/JpaAbstractDaoListeningExecutorService.java
@@ -22,6 +22,7 @@ import javax.annotation.PreDestroy;
import java.util.concurrent.Executors;
public abstract class JpaAbstractDaoListeningExecutorService {
+
protected ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
@PreDestroy
diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
index 7fddfae..170f9a6 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/sql/timeseries/JpaTimeseriesDao.java
@@ -17,9 +17,7 @@ package org.thingsboard.server.dao.sql.timeseries;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
@@ -36,10 +34,12 @@ import org.thingsboard.server.dao.timeseries.TimeseriesDao;
import org.thingsboard.server.dao.util.SqlDao;
import javax.annotation.Nullable;
+import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@@ -50,6 +50,8 @@ import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID;
@SqlDao
public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService implements TimeseriesDao {
+ private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
+
@Autowired
private TsKvRepository tsKvRepository;
@@ -232,7 +234,8 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
entity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
entity.setLongValue(tsKvEntry.getLongValue().orElse(null));
entity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
- return service.submit(() -> {
+ log.trace("Saving entity: " + entity);
+ return insertService.submit(() -> {
tsKvRepository.save(entity);
return null;
});
@@ -240,7 +243,7 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
@Override
public ListenableFuture<Void> savePartition(EntityId entityId, long tsKvEntryTs, String key, long ttl) {
- return service.submit(() -> null);
+ return insertService.submit(() -> null);
}
@Override
@@ -254,10 +257,15 @@ public class JpaTimeseriesDao extends JpaAbstractDaoListeningExecutorService imp
latestEntity.setDoubleValue(tsKvEntry.getDoubleValue().orElse(null));
latestEntity.setLongValue(tsKvEntry.getLongValue().orElse(null));
latestEntity.setBooleanValue(tsKvEntry.getBooleanValue().orElse(null));
- return service.submit(() -> {
+ return insertService.submit(() -> {
tsKvLatestRepository.save(latestEntity);
return null;
});
}
+ @PreDestroy
+ void onDestroy() {
+ insertService.shutdown();
+ }
+
}
extensions/extension-kafka/pom.xml 2(+1 -1)
diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml
index 6342be4..c53fc72 100644
--- a/extensions/extension-kafka/pom.xml
+++ b/extensions/extension-kafka/pom.xml
@@ -30,7 +30,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Kafka Extension</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
extensions/extension-mqtt/pom.xml 2(+1 -1)
diff --git a/extensions/extension-mqtt/pom.xml b/extensions/extension-mqtt/pom.xml
index 85f511d..517fbd3 100644
--- a/extensions/extension-mqtt/pom.xml
+++ b/extensions/extension-mqtt/pom.xml
@@ -30,7 +30,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server MQTT Extension</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml
index ba5e6d2..9f95670 100644
--- a/extensions/extension-rabbitmq/pom.xml
+++ b/extensions/extension-rabbitmq/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server RabbitMQ Extension</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml
index af5d196..a7872b8 100644
--- a/extensions/extension-rest-api-call/pom.xml
+++ b/extensions/extension-rest-api-call/pom.xml
@@ -30,7 +30,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server REST API Call Extension</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
extensions/extension-sns/pom.xml 81(+81 -0)
diff --git a/extensions/extension-sns/pom.xml b/extensions/extension-sns/pom.xml
new file mode 100644
index 0000000..ae31773
--- /dev/null
+++ b/extensions/extension-sns/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>extensions</artifactId>
+ <groupId>org.thingsboard</groupId>
+ <version>1.4.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.thingsboard.extensions</groupId>
+ <artifactId>extension-sns</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server SNS Extension</name>
+ <url>https://thingsboard.io</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ <aws.sdk.version>1.11.229</aws.sdk.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>extensions-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>extensions-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.amazonaws</groupId>
+ <artifactId>aws-java-sdk-sns</artifactId>
+ <version>${aws.sdk.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptors>
+ <descriptor>src/assembly/extension.xml</descriptor>
+ </descriptors>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/extensions/extension-sns/src/assembly/extension.xml b/extensions/extension-sns/src/assembly/extension.xml
new file mode 100644
index 0000000..2395d55
--- /dev/null
+++ b/extensions/extension-sns/src/assembly/extension.xml
@@ -0,0 +1,37 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
+ <id>extension</id>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <outputDirectory>/</outputDirectory>
+ <useProjectArtifact>true</useProjectArtifact>
+ <unpack>true</unpack>
+ <scope>runtime</scope>
+ <excludes>
+
+ </excludes>
+ </dependencySet>
+ </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java
new file mode 100644
index 0000000..b64a22b
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+public class SnsTopicActionMsg extends AbstractRuleToPluginMsg<SnsTopicActionPayload> {
+
+ public SnsTopicActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SnsTopicActionPayload payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java
new file mode 100644
index 0000000..a1bf89a
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicActionPayload.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.io.Serializable;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Data
+@Builder
+public class SnsTopicActionPayload implements Serializable {
+
+ private final String topicArn;
+ private final String msgBody;
+
+ private final Integer requestId;
+ private final MsgType msgType;
+ private final boolean sync;
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java
new file mode 100644
index 0000000..79de23a
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginAction.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
+
+import java.util.Optional;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Action(name = "SNS Topic Action", descriptor = "SnsTopicActionDescriptor.json", configuration = SnsTopicPluginActionConfiguration.class)
+public class SnsTopicPluginAction extends AbstractTemplatePluginAction<SnsTopicPluginActionConfiguration> {
+
+ @Override
+ protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
+ SnsTopicActionPayload.SnsTopicActionPayloadBuilder builder = SnsTopicActionPayload.builder();
+ builder.msgType(payload.getMsgType());
+ builder.requestId(payload.getRequestId());
+ builder.topicArn(configuration.getTopicArn());
+ builder.msgBody(getMsgBody(ctx, msg));
+ return Optional.of(new SnsTopicActionMsg(msg.getTenantId(),
+ msg.getCustomerId(),
+ msg.getDeviceId(),
+ builder.build()));
+ }
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java
new file mode 100644
index 0000000..468c3dd
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/action/SnsTopicPluginActionConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.action;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Data
+public class SnsTopicPluginActionConfiguration implements TemplateActionConfiguration {
+
+ private String topicArn;
+ private String template;
+ private boolean sync;
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java
new file mode 100644
index 0000000..c7fd99c
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsMessageHandler.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.plugin;
+
+import com.amazonaws.services.sns.AmazonSNS;
+import com.amazonaws.services.sns.model.PublishRequest;
+import com.amazonaws.services.sns.model.PublishResult;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.sns.action.SnsTopicActionMsg;
+import org.thingsboard.server.extensions.sns.action.SnsTopicActionPayload;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class SnsMessageHandler implements RuleMsgHandler {
+
+ private final AmazonSNS sns;
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ if (msg instanceof SnsTopicActionMsg) {
+ SnsTopicActionPayload payload = ((SnsTopicActionMsg) msg).getPayload();
+ PublishRequest publishRequest = new PublishRequest()
+ .withTopicArn(payload.getTopicArn())
+ .withMessage(payload.getMsgBody());
+ sns.publish(publishRequest);
+ if (payload.isSync()) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
+ BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
+ }
+ return;
+ }
+ throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
+
+ }
+
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java
new file mode 100644
index 0000000..86f5de0
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPlugin.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.plugin;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.sns.AmazonSNS;
+import com.amazonaws.services.sns.AmazonSNSClient;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.sns.action.SnsTopicPluginAction;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@Plugin(name = "SNS Plugin", actions = {SnsTopicPluginAction.class},
+ descriptor = "SnsPluginDescriptor.json", configuration = SnsPluginConfiguration.class)
+public class SnsPlugin extends AbstractPlugin<SnsPluginConfiguration> {
+
+ private SnsMessageHandler snsMessageHandler;
+ private SnsPluginConfiguration configuration;
+
+ @Override
+ public void init(SnsPluginConfiguration configuration) {
+ this.configuration = configuration;
+ init();
+ }
+
+ private void init() {
+ AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
+ AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials);
+ AmazonSNS sns = AmazonSNSClient.builder()
+ .withCredentials(credProvider)
+ .withRegion(configuration.getRegion())
+ .build();
+ this.snsMessageHandler = new SnsMessageHandler(sns);
+
+ }
+
+ private void destroy() {
+ this.snsMessageHandler = null;
+ }
+
+ @Override
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return snsMessageHandler;
+ }
+
+ @Override
+ public void resume(PluginContext ctx) {
+ init();
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+ destroy();
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+ destroy();
+ }
+}
diff --git a/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java
new file mode 100644
index 0000000..dee78f3
--- /dev/null
+++ b/extensions/extension-sns/src/main/java/org/thingsboard/server/extensions/sns/plugin/SnsPluginConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sns.plugin;
+
+import lombok.Data;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Data
+public class SnsPluginConfiguration {
+
+ private String accessKeyId;
+ private String secretAccessKey;
+ private String region;
+
+}
diff --git a/extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json b/extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json
new file mode 100644
index 0000000..04e23e7
--- /dev/null
+++ b/extensions/extension-sns/src/main/resources/SnsPluginDescriptor.json
@@ -0,0 +1,30 @@
+{
+ "schema": {
+ "title": "SNS Plugin Configuration",
+ "type": "object",
+ "properties": {
+ "accessKeyId": {
+ "title": "Access Key ID",
+ "type": "string"
+ },
+ "secretAccessKey": {
+ "title": "Secret Access Key",
+ "type": "string"
+ },
+ "region": {
+ "title": "Region",
+ "type": "string"
+ }
+ },
+ "required": [
+ "accessKeyId",
+ "secretAccessKey",
+ "region"
+ ]
+ },
+ "form": [
+ "accessKeyId",
+ "secretAccessKey",
+ "region"
+ ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json b/extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json
new file mode 100644
index 0000000..a8a2793
--- /dev/null
+++ b/extensions/extension-sns/src/main/resources/SnsTopicActionDescriptor.json
@@ -0,0 +1,34 @@
+{
+ "schema": {
+ "title": "SNS Topic Action Configuration",
+ "type": "object",
+ "properties": {
+ "sync": {
+ "title": "Requires delivery confirmation",
+ "type": "boolean"
+ },
+ "topicArn": {
+ "title": "Topic ARN",
+ "type": "string"
+ },
+ "template": {
+ "title": "Body Template",
+ "type": "string"
+ }
+ },
+ "required": [
+ "sync",
+ "topicArn",
+ "template"
+ ]
+ },
+ "form": [
+ "sync",
+ "topicArn",
+ {
+ "key": "template",
+ "type": "textarea",
+ "rows": 5
+ }
+ ]
+}
\ No newline at end of file
extensions/extension-sqs/pom.xml 81(+81 -0)
diff --git a/extensions/extension-sqs/pom.xml b/extensions/extension-sqs/pom.xml
new file mode 100644
index 0000000..7f3818d
--- /dev/null
+++ b/extensions/extension-sqs/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>extensions</artifactId>
+ <groupId>org.thingsboard</groupId>
+ <version>1.4.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.thingsboard.extensions</groupId>
+ <artifactId>extension-sqs</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Thingsboard Server SQS Extension</name>
+ <url>https://thingsboard.io</url>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <main.dir>${basedir}/../..</main.dir>
+ <aws.sdk.version>1.11.229</aws.sdk.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>extensions-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard</groupId>
+ <artifactId>extensions-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.amazonaws</groupId>
+ <artifactId>aws-java-sdk-sqs</artifactId>
+ <version>${aws.sdk.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptors>
+ <descriptor>src/assembly/extension.xml</descriptor>
+ </descriptors>
+ </configuration>
+ <executions>
+ <execution>
+ <id>make-assembly</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/assembly/extension.xml b/extensions/extension-sqs/src/assembly/extension.xml
new file mode 100644
index 0000000..2395d55
--- /dev/null
+++ b/extensions/extension-sqs/src/assembly/extension.xml
@@ -0,0 +1,37 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
+ <id>extension</id>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <outputDirectory>/</outputDirectory>
+ <useProjectArtifact>true</useProjectArtifact>
+ <unpack>true</unpack>
+ <scope>runtime</scope>
+ <excludes>
+
+ </excludes>
+ </dependencySet>
+ </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java
new file mode 100644
index 0000000..c465063
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+public class SqsFifoQueueActionMsg extends AbstractRuleToPluginMsg<SqsFifoQueueActionPayload> {
+
+ public SqsFifoQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsFifoQueueActionPayload payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java
new file mode 100644
index 0000000..692cd90
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueueActionPayload.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.io.Serializable;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+@Data
+@Builder
+public class SqsFifoQueueActionPayload implements Serializable {
+
+ private final String queue;
+ private final String msgBody;
+ private final String deviceId;
+
+ private final Integer requestId;
+ private final MsgType msgType;
+ private final boolean sync;
+
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java
new file mode 100644
index 0000000..f107ea1
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginAction.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginActionConfiguration;
+
+import java.util.Optional;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Action(name = "SQS Fifo Queue Action", descriptor = "SqsFifoQueueActionDescriptor.json", configuration = SqsFifoQueuePluginActionConfiguration.class)
+public class SqsFifoQueuePluginAction extends AbstractTemplatePluginAction<SqsFifoQueuePluginActionConfiguration> {
+
+ @Override
+ protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
+ SqsFifoQueueActionPayload.SqsFifoQueueActionPayloadBuilder builder = SqsFifoQueueActionPayload.builder();
+ builder.msgType(payload.getMsgType());
+ builder.requestId(payload.getRequestId());
+ builder.queue(configuration.getQueue());
+ builder.deviceId(msg.getDeviceId().toString());
+ builder.msgBody(getMsgBody(ctx, msg));
+ return Optional.of(new SqsFifoQueueActionMsg(msg.getTenantId(),
+ msg.getCustomerId(),
+ msg.getDeviceId(),
+ builder.build()));
+ }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java
new file mode 100644
index 0000000..d100325
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/fifo/SqsFifoQueuePluginActionConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.fifo;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+@Data
+public class SqsFifoQueuePluginActionConfiguration implements TemplateActionConfiguration {
+
+ private String queue;
+ private String template;
+ private boolean sync;
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java
new file mode 100644
index 0000000..6666b27
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+public class SqsStandardQueueActionMsg extends AbstractRuleToPluginMsg<SqsStandardQueueActionPayload> {
+
+ public SqsStandardQueueActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, SqsStandardQueueActionPayload payload) {
+ super(tenantId, customerId, deviceId, payload);
+ }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java
new file mode 100644
index 0000000..ce3dd27
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueueActionPayload.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import lombok.Builder;
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.io.Serializable;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@Data
+@Builder
+public class SqsStandardQueueActionPayload implements Serializable {
+
+ private final String queue;
+ private final String msgBody;
+ private final int delaySeconds;
+
+ private final Integer requestId;
+ private final MsgType msgType;
+ private final boolean sync;
+
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java
new file mode 100644
index 0000000..1dd9d56
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginAction.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.core.action.template.AbstractTemplatePluginAction;
+
+import java.util.Optional;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Action(name = "SQS Standard Queue Action", descriptor = "SqsStandardQueueActionDescriptor.json", configuration = SqsStandardQueuePluginActionConfiguration.class)
+public class SqsStandardQueuePluginAction extends AbstractTemplatePluginAction<SqsStandardQueuePluginActionConfiguration> {
+
+ @Override
+ protected Optional<RuleToPluginMsg> buildRuleToPluginMsg(RuleContext ctx, ToDeviceActorMsg msg, FromDeviceRequestMsg payload) {
+ SqsStandardQueueActionPayload.SqsStandardQueueActionPayloadBuilder builder = SqsStandardQueueActionPayload.builder();
+ builder.msgType(payload.getMsgType());
+ builder.requestId(payload.getRequestId());
+ builder.queue(configuration.getQueue());
+ builder.delaySeconds(configuration.getDelaySeconds());
+ builder.msgBody(getMsgBody(ctx, msg));
+ return Optional.of(new SqsStandardQueueActionMsg(msg.getTenantId(),
+ msg.getCustomerId(),
+ msg.getDeviceId(),
+ builder.build()));
+ }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java
new file mode 100644
index 0000000..5c21ef5
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/action/standard/SqsStandardQueuePluginActionConfiguration.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.action.standard;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.action.template.TemplateActionConfiguration;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@Data
+public class SqsStandardQueuePluginActionConfiguration implements TemplateActionConfiguration {
+
+ private String queue;
+ private int delaySeconds;
+ private boolean sync;
+ private String template;
+
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java
new file mode 100644
index 0000000..b71e0b1
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsMessageHandler.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.plugin;
+
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.model.SendMessageRequest;
+import com.amazonaws.services.sqs.model.SendMessageResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionMsg;
+import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueueActionPayload;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionMsg;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueueActionPayload;
+
+/**
+ * Created by Valerii Sosliuk on 11/15/2017.
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class SqsMessageHandler implements RuleMsgHandler {
+
+ private final AmazonSQS sqs;
+
+ @Override
+ public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+ if (msg instanceof SqsStandardQueueActionMsg) {
+ sendMessageToStandardQueue(ctx, tenantId, ruleId, msg);
+ return;
+ }
+ if (msg instanceof SqsFifoQueueActionMsg) {
+ sendMessageToFifoQueue(ctx, tenantId, ruleId, msg);
+ return;
+ }
+ throw new RuleException("Unsupported message type " + msg.getClass().getName() + "!");
+ }
+
+ private void sendMessageToStandardQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
+ SqsStandardQueueActionPayload payload = ((SqsStandardQueueActionMsg) msg).getPayload();
+ SendMessageRequest sendMsgRequest = new SendMessageRequest()
+ .withDelaySeconds(payload.getDelaySeconds())
+ .withQueueUrl(payload.getQueue())
+ .withMessageBody(payload.getMsgBody());
+ sqs.sendMessage(sendMsgRequest);
+ if (payload.isSync()) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
+ BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
+ }
+ }
+
+ private void sendMessageToFifoQueue(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
+ SqsFifoQueueActionPayload payload = ((SqsFifoQueueActionMsg) msg).getPayload();
+ SendMessageRequest sendMsgRequest = new SendMessageRequest()
+ .withQueueUrl(payload.getQueue())
+ .withMessageBody(payload.getMsgBody())
+ .withMessageGroupId(payload.getDeviceId());
+ sqs.sendMessage(sendMsgRequest);
+ if (payload.isSync()) {
+ ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId,
+ BasicStatusCodeResponse.onSuccess(payload.getMsgType(), payload.getRequestId())));
+ }
+ }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java
new file mode 100644
index 0000000..3f0252e
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPlugin.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.plugin;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.sqs.action.fifo.SqsFifoQueuePluginAction;
+import org.thingsboard.server.extensions.sqs.action.standard.SqsStandardQueuePluginAction;
+
+/**
+ * Created by Valerii Sosliuk on 11/6/2017.
+ */
+@Plugin(name = "SQS Plugin", actions = {SqsStandardQueuePluginAction.class, SqsFifoQueuePluginAction.class},
+ descriptor = "SqsPluginDescriptor.json", configuration = SqsPluginConfiguration.class)
+public class SqsPlugin extends AbstractPlugin<SqsPluginConfiguration> {
+
+ private SqsMessageHandler sqsMessageHandler;
+ private SqsPluginConfiguration configuration;
+
+ @Override
+ public void init(SqsPluginConfiguration configuration) {
+ this.configuration = configuration;
+ init();
+ }
+
+ private void init() {
+ AWSCredentials awsCredentials = new BasicAWSCredentials(configuration.getAccessKeyId(), configuration.getSecretAccessKey());
+ AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
+ .withRegion(Regions.fromName(configuration.getRegion())).build();
+ this.sqsMessageHandler = new SqsMessageHandler(sqs);
+
+ }
+
+ private void destroy() {
+ this.sqsMessageHandler = null;
+ }
+
+ @Override
+ protected RuleMsgHandler getRuleMsgHandler() {
+ return sqsMessageHandler;
+ }
+
+ @Override
+ public void resume(PluginContext ctx) {
+ init();
+ }
+
+ @Override
+ public void suspend(PluginContext ctx) {
+ destroy();
+ }
+
+ @Override
+ public void stop(PluginContext ctx) {
+ destroy();
+ }
+}
diff --git a/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java
new file mode 100644
index 0000000..a93b7f3
--- /dev/null
+++ b/extensions/extension-sqs/src/main/java/org/thingsboard/server/extensions/sqs/plugin/SqsPluginConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs.plugin;
+
+import lombok.Data;
+
+/**
+ * Created by Valerii Sosliuk on 11/5/2017.
+ */
+@Data
+public class SqsPluginConfiguration {
+
+ private String accessKeyId;
+ private String secretAccessKey;
+ private String region;
+
+}
diff --git a/extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json b/extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json
new file mode 100644
index 0000000..05bfaec
--- /dev/null
+++ b/extensions/extension-sqs/src/main/resources/SqsFifoQueueActionDescriptor.json
@@ -0,0 +1,34 @@
+{
+ "schema": {
+ "title": "SQS FIFO Queue Action Configuration",
+ "type": "object",
+ "properties": {
+ "sync": {
+ "title": "Requires delivery confirmation",
+ "type": "boolean"
+ },
+ "queue": {
+ "title": "Queue URL",
+ "type": "string"
+ },
+ "template": {
+ "title": "Body Template",
+ "type": "string"
+ }
+ },
+ "required": [
+ "sync",
+ "queue",
+ "template"
+ ]
+ },
+ "form": [
+ "sync",
+ "queue",
+ {
+ "key": "template",
+ "type": "textarea",
+ "rows": 5
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json b/extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json
new file mode 100644
index 0000000..407468b
--- /dev/null
+++ b/extensions/extension-sqs/src/main/resources/SqsPluginDescriptor.json
@@ -0,0 +1,30 @@
+{
+ "schema": {
+ "title": "SQS Plugin Configuration",
+ "type": "object",
+ "properties": {
+ "accessKeyId": {
+ "title": "Access Key ID",
+ "type": "string"
+ },
+ "secretAccessKey": {
+ "title": "Secret Access Key",
+ "type": "string"
+ },
+ "region": {
+ "title": "Region",
+ "type": "string"
+ }
+ },
+ "required": [
+ "accessKeyId",
+ "secretAccessKey",
+ "region"
+ ]
+ },
+ "form": [
+ "accessKeyId",
+ "secretAccessKey",
+ "region"
+ ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json b/extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json
new file mode 100644
index 0000000..f5502e6
--- /dev/null
+++ b/extensions/extension-sqs/src/main/resources/SqsStandardQueueActionDescriptor.json
@@ -0,0 +1,41 @@
+{
+ "schema": {
+ "title": "SQS Standard Queue Action Configuration",
+ "type": "object",
+ "properties": {
+ "sync": {
+ "title": "Requires delivery confirmation",
+ "type": "boolean"
+ },
+ "queue": {
+ "title": "Queue URL",
+ "type": "string"
+ },
+ "delaySeconds": {
+ "title": "Delay Seconds",
+ "type": "integer",
+ "default": 0
+ },
+ "template": {
+ "title": "Body Template",
+ "type": "string"
+ }
+ },
+ "required": [
+ "sync",
+ "queue",
+ "delaySeconds",
+ "template"
+ ]
+ },
+ "form": [
+ "sync",
+ "queue",
+ "delaySeconds",
+ {
+ "key": "template",
+ "type": "textarea",
+ "rows": 5
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java b/extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java
new file mode 100644
index 0000000..17c9b08
--- /dev/null
+++ b/extensions/extension-sqs/src/test/java/org/thingsboard/server/extensions/sqs/SqsDemoClient.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.sqs;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.regions.Regions;
+import com.amazonaws.services.sqs.AmazonSQS;
+import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
+import com.amazonaws.services.sqs.model.DeleteMessageRequest;
+import com.amazonaws.services.sqs.model.Message;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+/**
+ * Created by Valerii Sosliuk on 11/10/2017.
+ */
+@Slf4j
+public class SqsDemoClient {
+
+ private static final String ACCESS_KEY_ID = "$ACCES_KEY_ID";
+ private static final String SECRET_ACCESS_KEY = "$SECRET_ACCESS_KEY";
+
+ private static final String QUEUE_URL = "$QUEUE_URL";
+ private static final String REGION = "us-east-1";
+
+ public static void main(String[] args) {
+ log.info("Starting SQS Demo Clinent...");
+ AWSCredentials awsCredentials = new BasicAWSCredentials(ACCESS_KEY_ID, SECRET_ACCESS_KEY);
+ AmazonSQS sqs = AmazonSQSClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
+ .withRegion(Regions.fromName(REGION)).build();
+ SqsDemoClient client = new SqsDemoClient();
+ client.pollMessages(sqs);
+ }
+
+ private void pollMessages(AmazonSQS sqs) {
+ log.info("Polling messages");
+ while (true) {
+ List<Message> messages = sqs.receiveMessage(QUEUE_URL).getMessages();
+ messages.forEach(m -> {
+ log.info("Message Received: " + m.getBody());
+ System.out.println(m.getBody());
+ DeleteMessageRequest deleteMessageRequest = new DeleteMessageRequest(QUEUE_URL, m.getReceiptHandle());
+ sqs.deleteMessage(deleteMessageRequest);
+ });
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/extensions/extension-sqs/src/test/resources/logback.xml b/extensions/extension-sqs/src/test/resources/logback.xml
new file mode 100644
index 0000000..757aa32
--- /dev/null
+++ b/extensions/extension-sqs/src/test/resources/logback.xml
@@ -0,0 +1,10 @@
+<configuration>
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder>
+ <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+ </encoder>
+ </appender>
+ <root level="INFO">
+ <appender-ref ref="STDOUT"/>
+ </root>
+</configuration>
\ No newline at end of file
extensions/pom.xml 4(+3 -1)
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 30c9395..50dca83 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -28,7 +28,7 @@
<packaging>pom</packaging>
<name>Thingsboard Extensions</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<main.dir>${basedir}/..</main.dir>
@@ -39,6 +39,8 @@
<module>extension-rest-api-call</module>
<module>extension-kafka</module>
<module>extension-mqtt</module>
+ <module>extension-sqs</module>
+ <module>extension-sns</module>
</modules>
</project>
extensions-api/pom.xml 2(+1 -1)
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
index 1305c98..298da9a 100644
--- a/extensions-api/pom.xml
+++ b/extensions-api/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Extensions API</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
extensions-core/pom.xml 2(+1 -1)
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
index f94264e..14f91a1 100644
--- a/extensions-core/pom.xml
+++ b/extensions-core/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Core Extensions</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
index 0b8e992..7e92fc1 100644
--- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -128,12 +128,16 @@ public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
Optional<Long> interval = request.getLongParamValue("interval");
Optional<Integer> limit = request.getIntParamValue("limit");
+ // If some of these params are specified, they all must be
if (startTs.isPresent() || endTs.isPresent() || interval.isPresent() || limit.isPresent()) {
- if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent()) {
+ if (!startTs.isPresent() || !endTs.isPresent() || !interval.isPresent() || interval.get() < 0) {
msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
return;
}
- Aggregation agg = Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
+
+ // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
+ Aggregation agg = (interval.isPresent() && interval.get() == 0) ? Aggregation.valueOf(Aggregation.NONE.name()) :
+ Aggregation.valueOf(request.getParameter("agg", Aggregation.NONE.name()));
List<TsKvQuery> queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg))
.collect(Collectors.toList());
pom.xml 12(+12 -0)
diff --git a/pom.xml b/pom.xml
index 1f9a92d..e45fc3e 100755
--- a/pom.xml
+++ b/pom.xml
@@ -351,6 +351,18 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.thingsboard.extensions</groupId>
+ <artifactId>extension-sqs</artifactId>
+ <classifier>extension</classifier>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.thingsboard.extensions</groupId>
+ <artifactId>extension-sns</artifactId>
+ <classifier>extension</classifier>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
<version>${project.version}</version>
resume.bat 18(+18 -0)
diff --git a/resume.bat b/resume.bat
new file mode 100644
index 0000000..c3c0e1d
--- /dev/null
+++ b/resume.bat
@@ -0,0 +1,18 @@
+@REM
+@REM Copyright © 2016-2017 The Thingsboard Authors
+@REM
+@REM Licensed under the Apache License, Version 2.0 (the "License");
+@REM you may not use this file except in compliance with the License.
+@REM You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing, software
+@REM distributed under the License is distributed on an "AS IS" BASIS,
+@REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@REM See the License for the specific language governing permissions and
+@REM limitations under the License.
+@REM
+
+mvn clean install -rf :application
+
tools/pom.xml 2(+1 -1)
diff --git a/tools/pom.xml b/tools/pom.xml
index f0ade6e..5b74b56 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server Tools</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
index d635dbc..7000496 100644
--- a/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
+++ b/tools/src/main/java/org/thingsboard/client/tools/RestClient.java
@@ -26,9 +26,16 @@ import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
+import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.alarm.AlarmSeverity;
+import org.thingsboard.server.common.data.alarm.AlarmStatus;
+import org.thingsboard.server.common.data.asset.Asset;
+import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.io.IOException;
@@ -71,18 +78,40 @@ public class RestClient implements ClientHttpRequestInterceptor {
}
}
- public Device createDevice(String name) {
+ public Customer createCustomer(String title) {
+ Customer customer = new Customer();
+ customer.setTitle(title);
+ return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
+ }
+
+ public Device createDevice(String name, String type) {
Device device = new Device();
device.setName(name);
+ device.setType(type);
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
}
+ public Asset createAsset(String name, String type) {
+ Asset asset = new Asset();
+ asset.setName(name);
+ asset.setType(type);
+ return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
+ }
+
+ public Alarm createAlarm(Alarm alarm) {
+ return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
+ }
public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
customerId.toString(), deviceId.toString()).getBody();
}
+ public Asset assignAsset(CustomerId customerId, AssetId assetId) {
+ return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/asset/{assetId}", null, Asset.class,
+ customerId.toString(), assetId.toString()).getBody();
+ }
+
public DeviceCredentials getCredentials(DeviceId id) {
return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
}
@@ -91,11 +120,14 @@ public class RestClient implements ClientHttpRequestInterceptor {
return restTemplate;
}
+ public String getToken() {
+ return token;
+ }
+
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
HttpRequest wrapper = new HttpRequestWrapper(request);
wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
return execution.execute(wrapper, bytes);
}
-
-}
+}
\ No newline at end of file
tools/src/main/python/mqtt-send-telemetry.py 36(+36 -0)
diff --git a/tools/src/main/python/mqtt-send-telemetry.py b/tools/src/main/python/mqtt-send-telemetry.py
new file mode 100644
index 0000000..8c4263d
--- /dev/null
+++ b/tools/src/main/python/mqtt-send-telemetry.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2016-2017 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import paho.mqtt.client as mqtt
+from time import sleep
+import random
+
+broker="test.mosquitto.org"
+topic_pub='v1/devices/me/telemetry'
+
+
+client = mqtt.Client()
+
+client.username_pw_set("TEST_TOKEN")
+client.connect('127.0.0.1', 1883, 1)
+
+for i in range(5):
+ x = random.randrange(20, 100)
+ print x
+ msg = '{"windSpeed":"'+ str(x) + '"}'
+ client.publish(topic_pub, msg)
+ sleep(0.1)
transport/coap/pom.xml 2(+1 -1)
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
index c67b2a8..7212023 100644
--- a/transport/coap/pom.xml
+++ b/transport/coap/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard COAP Transport</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
transport/http/pom.xml 2(+1 -1)
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
index 58d5d64..dc5dfa5 100644
--- a/transport/http/pom.xml
+++ b/transport/http/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard HTTP Transport</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
transport/mqtt/pom.xml 2(+1 -1)
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
index 414ca9f..d8f5703 100644
--- a/transport/mqtt/pom.xml
+++ b/transport/mqtt/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard MQTT Transport</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
index 2e6abfd..7e4c2ea 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -229,6 +229,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
} else if (topicName.equals(DEVICE_ATTRIBUTES_RESPONSES_TOPIC)) {
deviceSessionCtx.setAllowAttributeResponses();
grantedQoSList.add(getMinSupportedQos(reqQoS));
+ } else if (topicName.equals(GATEWAY_ATTRIBUTES_TOPIC)) {
+ grantedQoSList.add(getMinSupportedQos(reqQoS));
} else {
log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
grantedQoSList.add(FAILURE.value());
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
index 7b527df..7bed03a 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.transport.mqtt.session;
import com.google.gson.Gson;
+import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.netty.buffer.ByteBuf;
@@ -24,6 +25,7 @@ import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.handler.codec.mqtt.*;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.msg.core.*;
import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
@@ -35,6 +37,7 @@ import org.thingsboard.server.transport.mqtt.MqttTopics;
import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
import java.nio.charset.Charset;
+import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
@@ -83,7 +86,7 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
if (responseMsg.isSuccess()) {
MsgType requestMsgType = responseMsg.getRequestMsgType();
Integer requestId = responseMsg.getRequestId();
- if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+ if (requestId >= 0 && requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
return Optional.of(MqttTransportHandler.createMqttPubAckMsg(requestId));
}
}
@@ -135,40 +138,43 @@ public class GatewayDeviceSessionCtx extends DeviceAwareSessionContext {
if (responseData.isPresent()) {
AttributesKVMsg msg = responseData.get();
if (msg.getClientAttributes() != null) {
- msg.getClientAttributes().forEach(v -> addValueToJson(result, "value", v));
+ addValues(result, msg.getClientAttributes());
}
if (msg.getSharedAttributes() != null) {
- msg.getSharedAttributes().forEach(v -> addValueToJson(result, "value", v));
+ addValues(result, msg.getSharedAttributes());
}
}
return createMqttPublishMsg(topic, result);
}
+ private void addValues(JsonObject result, List<AttributeKvEntry> kvList) {
+ if (kvList.size() == 1) {
+ addValueToJson(result, "value", kvList.get(0));
+ } else {
+ JsonObject values;
+ if (result.has("values")) {
+ values = result.get("values").getAsJsonObject();
+ } else {
+ values = new JsonObject();
+ result.add("values", values);
+ }
+ kvList.forEach(value -> addValueToJson(values, value.getKey(), value));
+ }
+ }
+
private void addValueToJson(JsonObject json, String name, KvEntry entry) {
switch (entry.getDataType()) {
case BOOLEAN:
- Optional<Boolean> booleanValue = entry.getBooleanValue();
- if (booleanValue.isPresent()) {
- json.addProperty(name, booleanValue.get());
- }
+ entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean));
break;
case STRING:
- Optional<String> stringValue = entry.getStrValue();
- if (stringValue.isPresent()) {
- json.addProperty(name, stringValue.get());
- }
+ entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString));
break;
case DOUBLE:
- Optional<Double> doubleValue = entry.getDoubleValue();
- if (doubleValue.isPresent()) {
- json.addProperty(name, doubleValue.get());
- }
+ entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble));
break;
case LONG:
- Optional<Long> longValue = entry.getLongValue();
- if (longValue.isPresent()) {
- json.addProperty(name, longValue.get());
- }
+ entry.getLongValue().ifPresent(aLong -> json.addProperty(name, aLong));
break;
}
}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
index 00d1f0c..d69341c 100644
--- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
@@ -41,10 +41,7 @@ import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import java.util.stream.Collectors;
import static org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor.validateJsonPayload;
@@ -186,24 +183,34 @@ public class GatewaySessionCtx {
}
}
- public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException {
- JsonElement json = validateJsonPayload(gatewaySessionId, mqttMsg.payload());
+ public void onDeviceAttributesRequest(MqttPublishMessage msg) throws AdaptorException {
+ JsonElement json = validateJsonPayload(gatewaySessionId, msg.payload());
if (json.isJsonObject()) {
JsonObject jsonObj = json.getAsJsonObject();
int requestId = jsonObj.get("id").getAsInt();
String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString();
boolean clientScope = jsonObj.get("client").getAsBoolean();
- String key = jsonObj.get("key").getAsString();
+ Set<String> keys;
+ if (jsonObj.has("key")) {
+ keys = Collections.singleton(jsonObj.get("key").getAsString());
+ } else {
+ JsonArray keysArray = jsonObj.get("keys").getAsJsonArray();
+ keys = new HashSet<>();
+ for (JsonElement keyObj : keysArray) {
+ keys.add(keyObj.getAsString());
+ }
+ }
BasicGetAttributesRequest request;
if (clientScope) {
- request = new BasicGetAttributesRequest(requestId, Collections.singleton(key), null);
+ request = new BasicGetAttributesRequest(requestId, keys, null);
} else {
- request = new BasicGetAttributesRequest(requestId, null, Collections.singleton(key));
+ request = new BasicGetAttributesRequest(requestId, null, keys);
}
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicToDeviceActorSessionMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, request)));
+ ack(msg);
} else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
@@ -251,7 +258,7 @@ public class GatewaySessionCtx {
}
private void ack(MqttPublishMessage msg) {
- if(msg.variableHeader().messageId() > 0) {
+ if (msg.variableHeader().messageId() > 0) {
writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(msg.variableHeader().messageId()));
}
}
transport/pom.xml 2(+1 -1)
diff --git a/transport/pom.xml b/transport/pom.xml
index f9b67e3..63b1fe1 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -28,7 +28,7 @@
<packaging>pom</packaging>
<name>Thingsboard Server Transport Modules</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<main.dir>${basedir}/..</main.dir>
ui/package.json 1(+1 -0)
diff --git a/ui/package.json b/ui/package.json
index cb8fdbd..81f4574 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -30,6 +30,7 @@
"angular-material": "1.1.1",
"angular-material-data-table": "^0.10.9",
"angular-material-icons": "^0.7.1",
+ "angular-material-expansion-panel": "^0.7.2",
"angular-messages": "1.5.8",
"angular-route": "1.5.8",
"angular-sanitize": "1.5.8",
ui/pom.xml 2(+1 -1)
diff --git a/ui/pom.xml b/ui/pom.xml
index d84eaf5..e840067 100644
--- a/ui/pom.xml
+++ b/ui/pom.xml
@@ -28,7 +28,7 @@
<packaging>jar</packaging>
<name>Thingsboard Server UI</name>
- <url>http://thingsboard.org</url>
+ <url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
ui/src/app/api/login.service.js 16(+8 -8)
diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js
index 272e4df..74f9587 100644
--- a/ui/src/app/api/login.service.js
+++ b/ui/src/app/api/login.service.js
@@ -65,8 +65,8 @@ function LoginService($http, $q) {
function sendResetPasswordLink(email) {
var deferred = $q.defer();
- var url = '/api/noauth/resetPasswordByEmail?email=' + email;
- $http.post(url, null).then(function success(response) {
+ var url = '/api/noauth/resetPasswordByEmail';
+ $http.post(url, {email: email}).then(function success(response) {
deferred.resolve(response);
}, function fail() {
deferred.reject();
@@ -76,8 +76,8 @@ function LoginService($http, $q) {
function resetPassword(resetToken, password) {
var deferred = $q.defer();
- var url = '/api/noauth/resetPassword?resetToken=' + resetToken + '&password=' + password;
- $http.post(url, null).then(function success(response) {
+ var url = '/api/noauth/resetPassword';
+ $http.post(url, {resetToken: resetToken, password: password}).then(function success(response) {
deferred.resolve(response);
}, function fail() {
deferred.reject();
@@ -87,8 +87,8 @@ function LoginService($http, $q) {
function activate(activateToken, password) {
var deferred = $q.defer();
- var url = '/api/noauth/activate?activateToken=' + activateToken + '&password=' + password;
- $http.post(url, null).then(function success(response) {
+ var url = '/api/noauth/activate';
+ $http.post(url, {activateToken: activateToken, password: password}).then(function success(response) {
deferred.resolve(response);
}, function fail() {
deferred.reject();
@@ -98,8 +98,8 @@ function LoginService($http, $q) {
function changePassword(currentPassword, newPassword) {
var deferred = $q.defer();
- var url = '/api/auth/changePassword?currentPassword=' + currentPassword + '&newPassword=' + newPassword;
- $http.post(url, null).then(function success(response) {
+ var url = '/api/auth/changePassword';
+ $http.post(url, {currentPassword: currentPassword, newPassword: newPassword}).then(function success(response) {
deferred.resolve(response);
}, function fail() {
deferred.reject();
ui/src/app/api/user.service.js 19(+10 -9)
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index 1668657..d09387b 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -302,7 +302,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
$rootScope.forceFullscreen = true;
fetchAllowedDashboardIds();
} else if (currentUser.userId) {
- getUser(currentUser.userId).then(
+ getUser(currentUser.userId, true).then(
function success(user) {
currentUserDetails = user;
updateUserLang();
@@ -319,6 +319,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
},
function fail() {
deferred.reject();
+ logout();
}
)
} else {
@@ -414,19 +415,19 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
}
$http.post(url, user).then(function success(response) {
deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
- function getUser(userId) {
+ function getUser(userId, ignoreErrors) {
var deferred = $q.defer();
var url = '/api/user/' + userId;
- $http.get(url).then(function success(response) {
+ $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
deferred.resolve(response.data);
- }, function fail(response) {
- deferred.reject(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
@@ -436,8 +437,8 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi
var url = '/api/user/' + userId;
$http.delete(url).then(function success() {
deferred.resolve();
- }, function fail(response) {
- deferred.reject(response.data);
+ }, function fail() {
+ deferred.reject();
});
return deferred.promise;
}
ui/src/app/api/widget.service.js 3(+2 -1)
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 0df17b7..3c74ecb 100644
--- a/ui/src/app/api/widget.service.js
+++ b/ui/src/app/api/widget.service.js
@@ -21,6 +21,7 @@ import thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
+import thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@@ -42,7 +43,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
- thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
+ thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
.factory('widgetService', WidgetService)
.name;
ui/src/app/app.js 3(+3 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 4a6f3f0..5b19089 100644
--- a/ui/src/app/app.js
+++ b/ui/src/app/app.js
@@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router';
import angularJwt from 'angular-jwt';
import 'angular-drag-and-drop-lists';
import mdDataTable from 'angular-material-data-table';
+import 'angular-material-expansion-panel';
import ngTouch from 'angular-touch';
import 'angular-carousel';
import 'clipboard';
@@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css';
import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
+import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
import '../scss/main.scss';
import AppConfig from './app.config';
@@ -103,6 +105,7 @@ angular.module('thingsboard', [
angularJwt,
'dndLists',
mdDataTable,
+ 'material.components.expansionPanels',
ngTouch,
'angular-carousel',
'ngclipboard',
ui/src/app/asset/asset-card.tpl.html 2(+1 -1)
diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html
index 30d0483..b02b2e2 100644
--- a/ui/src/app/asset/asset-card.tpl.html
+++ b/ui/src/app/asset/asset-card.tpl.html
@@ -16,7 +16,7 @@
-->
<div flex layout="column" style="margin-top: -10px;">
- <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+ <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
</div>
diff --git a/ui/src/app/asset/assign-to-customer.controller.js b/ui/src/app/asset/assign-to-customer.controller.js
index 602e599..3df90ef 100644
--- a/ui/src/app/asset/assign-to-customer.controller.js
+++ b/ui/src/app/asset/assign-to-customer.controller.js
@@ -77,8 +77,8 @@ export default function AssignAssetToCustomerController(customerService, assetSe
function assign() {
var tasks = [];
- for (var assetId in assetIds) {
- tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[assetId]));
+ for (var i=0;i<assetIds.length;i++) {
+ tasks.push(assetService.assignAssetToCustomer(vm.customers.selection.id.id, assetIds[i]));
}
$q.all(tasks).then(function () {
$mdDialog.hide();
ui/src/app/common/dashboard-utils.service.js 18(+16 -2)
diff --git a/ui/src/app/common/dashboard-utils.service.js b/ui/src/app/common/dashboard-utils.service.js
index 64a2c64..c4b2486 100644
--- a/ui/src/app/common/dashboard-utils.service.js
+++ b/ui/src/app/common/dashboard-utils.service.js
@@ -425,12 +425,26 @@ function DashboardUtils(types, utils, timeService) {
var prevColumns = prevGridSettings ? prevGridSettings.columns : 24;
var ratio = gridSettings.columns / prevColumns;
layout.gridSettings = gridSettings;
+ var maxRow = 0;
for (var w in layout.widgets) {
var widget = layout.widgets[w];
+ maxRow = Math.max(maxRow, widget.row + widget.sizeY);
+ }
+ var newMaxRow = Math.round(maxRow * ratio);
+ for (w in layout.widgets) {
+ widget = layout.widgets[w];
+ if (widget.row + widget.sizeY == maxRow) {
+ widget.row = Math.round(widget.row * ratio);
+ widget.sizeY = newMaxRow - widget.row;
+ } else {
+ widget.row = Math.round(widget.row * ratio);
+ widget.sizeY = Math.round(widget.sizeY * ratio);
+ }
widget.sizeX = Math.round(widget.sizeX * ratio);
- widget.sizeY = Math.round(widget.sizeY * ratio);
widget.col = Math.round(widget.col * ratio);
- widget.row = Math.round(widget.row * ratio);
+ if (widget.col + widget.sizeX > gridSettings.columns) {
+ widget.sizeX = gridSettings.columns - widget.col;
+ }
}
}
ui/src/app/common/types.constant.js 47(+47 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index 7bac29d..5b3c9e4 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
name: "event.type-stats"
}
},
+ extensionType: {
+ http: "HTTP",
+ mqtt: "MQTT",
+ opc: "OPC UA"
+ },
+ extensionValueType: {
+ string: 'value.string',
+ long: 'value.long',
+ double: 'value.double',
+ boolean: 'value.boolean'
+ },
+ extensionTransformerType: {
+ toDouble: 'extension.to-double',
+ custom: 'extension.custom'
+ },
+ mqttConverterTypes: {
+ json: 'extension.converter-json',
+ custom: 'extension.custom'
+ },
+ mqttCredentialTypes: {
+ anonymous: {
+ value: "anonymous",
+ name: "extension.anonymous"
+ },
+ basic: {
+ value: "basic",
+ name: "extension.basic"
+ },
+ pem: {
+ value: "cert.PEM",
+ name: "extension.pem"
+ }
+ },
+ extensionOpcSecurityTypes: {
+ Basic128Rsa15: "Basic128Rsa15",
+ Basic256: "Basic256",
+ Basic256Sha256: "Basic256Sha256",
+ None: "None"
+ },
+ extensionIdentityType: {
+ anonymous: "extension.anonymous",
+ username: "extension.username"
+ },
+ extensionKeystoreType: {
+ PKCS12: "PKCS12",
+ JKS: "JKS"
+ },
latestTelemetry: {
value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry",
diff --git a/ui/src/app/components/confirm-on-exit.directive.js b/ui/src/app/components/confirm-on-exit.directive.js
index f27a9a5..28b0734 100644
--- a/ui/src/app/components/confirm-on-exit.directive.js
+++ b/ui/src/app/components/confirm-on-exit.directive.js
@@ -18,17 +18,17 @@ export default angular.module('thingsboard.directives.confirmOnExit', [])
.name;
/*@ngInject*/
-function ConfirmOnExit($state, $mdDialog, $window, $filter) {
+function ConfirmOnExit($state, $mdDialog, $window, $filter, userService) {
return {
link: function ($scope) {
$window.onbeforeunload = function () {
- if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
+ if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
return $filter('translate')('confirm-on-exit.message');
}
}
$scope.$on('$stateChangeStart', function (event, next, current, params) {
- if (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty) {
+ if (userService.isAuthenticated() && (($scope.confirmForm && $scope.confirmForm.$dirty) || $scope.isDirty)) {
event.preventDefault();
var confirm = $mdDialog.confirm()
.title($filter('translate')('confirm-on-exit.title'))
ui/src/app/components/dashboard.directive.js 177(+88 -89)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 6605692..c489c32 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -140,6 +140,8 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
vm.widgetLayoutInfo = {
};
+ vm.widgetIds = [];
+
vm.widgetItemMap = {
sizeX: 'vm.widgetLayoutInfo[widget.id].sizeX',
sizeY: 'vm.widgetLayoutInfo[widget.id].sizeY',
@@ -233,73 +235,12 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
removeResizeListener(gridsterParent[0], onGridsterParentResize); // eslint-disable-line no-undef
});
- watchWidgets();
-
function onGridsterParentResize() {
if (gridsterParent.height() && autofillHeight()) {
updateMobileOpts();
}
}
- function watchWidgets() {
- $scope.widgetsCollectionWatch = $scope.$watchCollection('vm.widgets', function () {
- if (vm.skipInitialWidgetsWatch) {
- $timeout(function() { vm.skipInitialWidgetsWatch = false; });
- return;
- }
- var ids = [];
- for (var i=0;i<vm.widgets.length;i++) {
- var widget = vm.widgets[i];
- if (!widget.id) {
- widget.id = utils.guid();
- }
- ids.push(widget.id);
- var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
- if (!layoutInfoObject) {
- layoutInfoObject = {
- widget: widget
- };
- Object.defineProperty(layoutInfoObject, 'sizeX', {
- get: function() { return widgetSizeX(this.widget) },
- set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
- });
- Object.defineProperty(layoutInfoObject, 'sizeY', {
- get: function() { return widgetSizeY(this.widget) },
- set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
- });
- Object.defineProperty(layoutInfoObject, 'row', {
- get: function() { return widgetRow(this.widget) },
- set: function(newRow) { setWidgetRow(this.widget, newRow)}
- });
- Object.defineProperty(layoutInfoObject, 'col', {
- get: function() { return widgetCol(this.widget) },
- set: function(newCol) { setWidgetCol(this.widget, newCol)}
- });
- vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
- }
- }
- for (var widgetId in vm.widgetLayoutInfo) {
- if (ids.indexOf(widgetId) === -1) {
- delete vm.widgetLayoutInfo[widgetId];
- }
- }
- $mdUtil.nextTick(function () {
- sortWidgets();
- if (autofillHeight()) {
- updateMobileOpts();
- }
- });
- });
- }
-
- function stopWatchWidgets() {
- if ($scope.widgetsCollectionWatch) {
- $scope.widgetsCollectionWatch();
- $scope.widgetsCollectionWatch = null;
- }
- }
-
-
//TODO: widgets visibility
/*gridsterParent.scroll(function () {
updateVisibleRect();
@@ -344,30 +285,6 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
return isMobileSize;
}
- $scope.$watch(function() { return $mdMedia('gt-sm'); }, function() {
- updateMobileOpts();
- });
-
- $scope.$watch('vm.isMobile', function () {
- updateMobileOpts();
- });
-
- $scope.$watch('vm.autofillHeight', function () {
- updateMobileOpts();
- });
-
- $scope.$watch('vm.mobileAutofillHeight', function () {
- updateMobileOpts();
- });
-
- $scope.$watch('vm.mobileRowHeight', function () {
- updateMobileOpts();
- });
-
- $scope.$watch('vm.isMobileDisabled', function () {
- updateMobileOpts();
- });
-
$scope.$watch('vm.columns', function () {
var columns = vm.columns ? vm.columns : 24;
if (vm.gridsterOpts.columns != columns) {
@@ -381,6 +298,19 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}
});
+ $scope.$watch(function() {
+ return $mdMedia('gt-sm') + ',' + vm.isMobile + ',' + vm.isMobileDisabled;
+ }, function() {
+ updateMobileOpts();
+ sortWidgets();
+ });
+
+ $scope.$watch(function() {
+ return vm.autofillHeight + ',' + vm.mobileAutofillHeight + ',' + vm.mobileRowHeight;
+ }, function () {
+ updateMobileOpts();
+ });
+
$scope.$watch('vm.margins', function () {
var margins = vm.margins ? vm.margins : [10, 10];
if (!angular.equals(vm.gridsterOpts.margins, margins)) {
@@ -407,9 +337,70 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
}
});
+ $scope.$watchCollection('vm.widgets', function () {
+ var ids = [];
+ for (var i=0;i<vm.widgets.length;i++) {
+ var widget = vm.widgets[i];
+ if (!widget.id) {
+ widget.id = utils.guid();
+ }
+ ids.push(widget.id);
+ }
+ ids.sort(function (id1, id2) {
+ return id1.localeCompare(id2);
+ });
+ if (angular.equals(ids, vm.widgetIds)) {
+ return;
+ }
+ vm.widgetIds = ids;
+ for (i=0;i<vm.widgets.length;i++) {
+ widget = vm.widgets[i];
+ var layoutInfoObject = vm.widgetLayoutInfo[widget.id];
+ if (!layoutInfoObject) {
+ layoutInfoObject = {
+ widget: widget
+ };
+ Object.defineProperty(layoutInfoObject, 'sizeX', {
+ get: function() { return widgetSizeX(this.widget) },
+ set: function(newSizeX) { setWidgetSizeX(this.widget, newSizeX)}
+ });
+ Object.defineProperty(layoutInfoObject, 'sizeY', {
+ get: function() { return widgetSizeY(this.widget) },
+ set: function(newSizeY) { setWidgetSizeY(this.widget, newSizeY)}
+ });
+ Object.defineProperty(layoutInfoObject, 'row', {
+ get: function() { return widgetRow(this.widget) },
+ set: function(newRow) { setWidgetRow(this.widget, newRow)}
+ });
+ Object.defineProperty(layoutInfoObject, 'col', {
+ get: function() { return widgetCol(this.widget) },
+ set: function(newCol) { setWidgetCol(this.widget, newCol)}
+ });
+ vm.widgetLayoutInfo[widget.id] = layoutInfoObject;
+ }
+ }
+ for (var widgetId in vm.widgetLayoutInfo) {
+ if (ids.indexOf(widgetId) === -1) {
+ delete vm.widgetLayoutInfo[widgetId];
+ }
+ }
+ sortWidgets();
+ $mdUtil.nextTick(function () {
+ if (autofillHeight()) {
+ updateMobileOpts();
+ }
+ });
+ });
+
+ $scope.$watch('vm.widgetLayouts', function () {
+ updateMobileOpts();
+ sortWidgets();
+ });
+
$scope.$on('gridster-resized', function (event, sizes, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
+ setupGridster(vm.gridster);
vm.isResizing = false;
//TODO: widgets visibility
//updateVisibleRect(false, true);
@@ -419,6 +410,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.$on('gridster-mobile-changed', function (event, theGridster) {
if (checkIsLocalGridsterElement(theGridster)) {
vm.gridster = theGridster;
+ setupGridster(vm.gridster);
detectRowSize(vm.gridster.isMobile).then(
function(rowHeight) {
if (vm.gridsterOpts.rowHeight != rowHeight) {
@@ -517,18 +509,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
loadDashboard();
function sortWidgets() {
- stopWatchWidgets();
vm.widgets.sort(function (widget1, widget2) {
var row1 = widgetOrder(widget1);
var row2 = widgetOrder(widget2);
var res = row1 - row2;
if (res === 0) {
- res = widget1.col - widget2.col;
+ res = widgetCol(widget1) - widgetCol(widget2);
}
return res;
});
- vm.skipInitialWidgetsWatch = true;
- watchWidgets();
}
function reload() {
@@ -1037,6 +1026,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
$scope.gridsterScopeWatcher = null;
var gridsterScope = gridsterElement.scope();
vm.gridster = gridsterScope.gridster;
+ setupGridster(vm.gridster);
if (vm.onInit) {
vm.onInit({dashboard: vm});
}
@@ -1046,6 +1036,15 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
});
}
+ function setupGridster(gridster) {
+ if (gridster) {
+ if (!gridster.origMoveOverlappingItems) {
+ gridster.origMoveOverlappingItems = gridster.moveOverlappingItems;
+ gridster.moveOverlappingItems = () => {};
+ }
+ }
+ }
+
function loading() {
return !vm.ignoreLoading && $rootScope.loading;
}
diff --git a/ui/src/app/components/details-sidenav.scss b/ui/src/app/components/details-sidenav.scss
index 54a029d..159162a 100644
--- a/ui/src/app/components/details-sidenav.scss
+++ b/ui/src/app/components/details-sidenav.scss
@@ -16,7 +16,10 @@
@import '../../scss/constants';
.tb-details-title {
- font-size: 1.600rem;
+ font-size: 1.000rem;
+ @media (min-width: $layout-breakpoint-gt-sm) {
+ font-size: 1.600rem;
+ }
font-weight: 400;
text-transform: uppercase;
margin: 20px 8px 0 0;
ui/src/app/components/js-func.scss 2(+1 -1)
diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss
index 7c36b81..3124dc1 100644
--- a/ui/src/app/components/js-func.scss
+++ b/ui/src/app/components/js-func.scss
@@ -22,7 +22,7 @@ tb-js-func {
border: 1px solid #C0C0C0;
height: 100%;
#tb-javascript-input {
- min-width: 400px;
+ min-width: 200px;
min-height: 200px;
width: 100%;
height: 100%;
diff --git a/ui/src/app/components/js-func.tpl.html b/ui/src/app/components/js-func.tpl.html
index ee3c2e0..bec7991 100644
--- a/ui/src/app/components/js-func.tpl.html
+++ b/ui/src/app/components/js-func.tpl.html
@@ -19,7 +19,7 @@
<div layout="row" layout-align="start center" style="height: 40px;">
<span style="font-style: italic;">function({{ functionArgsString }}) {</span>
<span flex></span>
- <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
+ <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
</div>
<div flex id="tb-javascript-panel" class="tb-js-func-panel" layout="column">
<div flex id="tb-javascript-input"
diff --git a/ui/src/app/device/assign-to-customer.controller.js b/ui/src/app/device/assign-to-customer.controller.js
index c04a200..b58435d 100644
--- a/ui/src/app/device/assign-to-customer.controller.js
+++ b/ui/src/app/device/assign-to-customer.controller.js
@@ -77,8 +77,8 @@ export default function AssignDeviceToCustomerController(customerService, device
function assign() {
var tasks = [];
- for (var deviceId in deviceIds) {
- tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[deviceId]));
+ for (var i=0;i<deviceIds.length;i++) {
+ tasks.push(deviceService.assignDeviceToCustomer(vm.customers.selection.id.id, deviceIds[i]));
}
$q.all(tasks).then(function () {
$mdDialog.hide();
diff --git a/ui/src/app/device/device-card.tpl.html b/ui/src/app/device/device-card.tpl.html
index 58f4c85..d7cd588 100644
--- a/ui/src/app/device/device-card.tpl.html
+++ b/ui/src/app/device/device-card.tpl.html
@@ -16,7 +16,7 @@
-->
<div flex layout="column" style="margin-top: -10px;">
- <div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
+ <div style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
</div>
ui/src/app/device/devices.tpl.html 7(+7 -0)
diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html
index b8394c7..1ec0134 100644
--- a/ui/src/app/device/devices.tpl.html
+++ b/ui/src/app/device/devices.tpl.html
@@ -67,4 +67,11 @@
entity-type="{{vm.types.entityType.device}}">
</tb-relation-table>
</md-tab>
+ <md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
+ <tb-extension-table flex
+ entity-id="vm.grid.operatingItem().id.id"
+ entity-name="vm.grid.operatingItem().name"
+ entity-type="{{vm.types.entityType.device}}">
+ </tb-extension-table>
+ </md-tab>
</tb-grid>
diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html
index 6f862ba..3b19e9d 100644
--- a/ui/src/app/entity/attribute/attribute-table.tpl.html
+++ b/ui/src/app/entity/attribute/attribute-table.tpl.html
@@ -16,7 +16,7 @@
-->
<md-content flex class="md-padding tb-absolute-fill" layout="column">
- <section layout="row" ng-show="!disableAttributeScopeSelection">
+ <section ng-show="!disableAttributeScopeSelection">
<md-input-container class="md-block" style="width: 200px;">
<label translate>attribute.attributes-scope</label>
<md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
@@ -26,7 +26,7 @@
</md-select>
</md-input-container>
</section>
- <div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
+ <div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
<md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
&& !selectedAttributes.length
&& query.search === null">
diff --git a/ui/src/app/entity/relation/relation-dialog.controller.js b/ui/src/app/entity/relation/relation-dialog.controller.js
index ae50e30..d13b4db 100644
--- a/ui/src/app/entity/relation/relation-dialog.controller.js
+++ b/ui/src/app/entity/relation/relation-dialog.controller.js
@@ -79,10 +79,8 @@ export default function RelationDialogController($scope, $mdDialog, types, entit
});
function updateEditorSize(element) {
- var newWidth = 600;
var newHeight = 200;
- angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px")
- .width(newWidth.toString() + "px");
+ angular.element('#tb-relation-additional-info', element).height(newHeight.toString() + "px");
vm.editor.resize();
}
diff --git a/ui/src/app/entity/relation/relation-dialog.scss b/ui/src/app/entity/relation/relation-dialog.scss
index 55064df..1bd038b 100644
--- a/ui/src/app/entity/relation/relation-dialog.scss
+++ b/ui/src/app/entity/relation/relation-dialog.scss
@@ -19,7 +19,7 @@
border: 1px solid #C0C0C0;
height: 100%;
#tb-relation-additional-info {
- min-width: 600px;
+ min-width: 200px;
min-height: 200px;
width: 100%;
height: 100%;
diff --git a/ui/src/app/entity/relation/relation-dialog.tpl.html b/ui/src/app/entity/relation/relation-dialog.tpl.html
index 15b0c7f..8799287 100644
--- a/ui/src/app/entity/relation/relation-dialog.tpl.html
+++ b/ui/src/app/entity/relation/relation-dialog.tpl.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 400px;">
+<md-dialog aria-label="{{ (vm.isAdd ? 'relation.add' : 'relation.edit' ) | translate }}" style="min-width: 600px;">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
ui/src/app/extension/extension-dialog.controller.js 298(+298 -0)
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js
new file mode 100644
index 0000000..fe5825c
--- /dev/null
+++ b/ui/src/app/extension/extension-dialog.controller.js
@@ -0,0 +1,298 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import beautify from 'js-beautify';
+
+const js_beautify = beautify.js;
+
+/*@ngInject*/
+export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
+
+ var vm = this;
+
+ vm.types = types;
+ vm.isAdd = isAdd;
+ vm.entityType = entityType;
+ vm.entityId = entityId;
+ vm.allExtensions = allExtensions;
+
+
+ if (extension) {
+ vm.extension = angular.copy(extension);
+ editTransformers(vm.extension);
+ } else {
+ vm.extension = {};
+ }
+
+
+ vm.extensionTypeChange = function () {
+
+ if (vm.extension.type === "HTTP") {
+ vm.extension.configuration = {
+ "converterConfigurations": []
+ };
+ }
+ if (vm.extension.type === "MQTT") {
+ vm.extension.configuration = {
+ "brokers": []
+ };
+ }
+ if (vm.extension.type === "OPC UA") {
+ vm.extension.configuration = {
+ "servers": []
+ };
+ }
+ };
+
+ vm.cancel = cancel;
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ vm.save = save;
+ function save() {
+ let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
+
+ if ($errorElement.length) {
+
+ let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
+ let $mdDialogTop = angular.element('md-dialog-content').offset().top;
+ let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
+
+
+ if ($errorElementTop !== $mdDialogTop) {
+ angular.element('md-dialog-content').animate({
+ scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50
+ }, 500);
+ $errorElement.eq(0).focus();
+ }
+ } else {
+
+ if(vm.isAdd) {
+ vm.allExtensions.push(vm.extension);
+ } else {
+ var index = vm.allExtensions.indexOf(extension);
+ if(index > -1) {
+ vm.allExtensions[index] = vm.extension;
+ }
+ }
+
+ $mdDialog.hide();
+ saveTransformers();
+
+ var editedValue = angular.toJson(vm.allExtensions);
+
+ attributeService
+ .saveEntityAttributes(
+ vm.entityType,
+ vm.entityId,
+ types.attributesScope.shared.value,
+ [{key:"configuration", value:editedValue}]
+ )
+ .then(function success() {
+ });
+
+ }
+ }
+
+ vm.validateId = function() {
+ var coincidenceArray = vm.allExtensions.filter(function(ext) {
+ return ext.id == vm.extension.id;
+ });
+ if(coincidenceArray.length) {
+ if(!vm.isAdd) {
+ if(coincidenceArray[0].id == extension.id) {
+ $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
+ } else {
+ $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
+ }
+ } else {
+ $scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
+ }
+ } else {
+ $scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
+ }
+ };
+
+ function saveTransformers() {
+ if(vm.extension.type == types.extensionType.http) {
+ var config = vm.extension.configuration.converterConfigurations;
+ if(config && config.length > 0) {
+ for(let i=0;i<config.length;i++) {
+ for(let j=0;j<config[i].converters.length;j++){
+ for(let k=0;k<config[i].converters[j].attributes.length;k++){
+ if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
+ config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
+ }
+ delete config[i].converters[j].attributes[k].transformerType;
+ }
+ for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
+ if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
+ config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
+ }
+ delete config[i].converters[j].timeseries[l].transformerType;
+ }
+ }
+ }
+ }
+ }
+ if(vm.extension.type == types.extensionType.mqtt) {
+ var brokers = vm.extension.configuration.brokers;
+ if(brokers && brokers.length > 0) {
+ for(let i=0;i<brokers.length;i++) {
+ if(brokers[i].mapping && brokers[i].mapping.length > 0) {
+ for(let j=0;j<brokers[i].mapping.length;j++) {
+ if(brokers[i].mapping[j].converterType == "json") {
+ delete brokers[i].mapping[j].converter.nameExp;
+ delete brokers[i].mapping[j].converter.typeExp;
+ }
+ delete brokers[i].mapping[j].converterType;
+ }
+ }
+ if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
+ for(let j=0;j<brokers[i].connectRequests.length;j++) {
+ delete brokers[i].connectRequests[j].nameExp;
+ }
+ }
+ if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
+ for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
+ delete brokers[i].disconnectRequests[j].nameExp;
+ }
+ }
+ if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
+ for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+ delete brokers[i].attributeRequests[j].nameExp;
+ }
+ for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+ delete brokers[i].attributeRequests[j].attrKey;
+ }
+ for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+ delete brokers[i].attributeRequests[j].requestId;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function editTransformers(extension) {
+ if(extension.type == types.extensionType.http) {
+ var config = extension.configuration.converterConfigurations;
+ for(let i=0;i<config.length;i++) {
+ for(let j=0;j<config[i].converters.length;j++){
+ for(let k=0;k<config[i].converters[j].attributes.length;k++){
+ if(config[i].converters[j].attributes[k].transformer){
+ if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
+ config[i].converters[j].attributes[k].transformerType = "toDouble";
+ } else {
+ config[i].converters[j].attributes[k].transformerType = "custom";
+ config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
+ }
+ }
+ }
+ for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
+ if(config[i].converters[j].timeseries[l].transformer){
+ if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
+ config[i].converters[j].timeseries[l].transformerType = "toDouble";
+ } else {
+ config[i].converters[j].timeseries[l].transformerType = "custom";
+ config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
+ }
+ }
+ }
+ }
+ }
+ }
+ if(extension.type == types.extensionType.mqtt) {
+ var brokers = extension.configuration.brokers;
+ for(let i=0;i<brokers.length;i++) {
+ if(brokers[i].mapping && brokers[i].mapping.length > 0) {
+ for(let j=0;j<brokers[i].mapping.length;j++) {
+ if(brokers[i].mapping[j].converter.type == "json") {
+ if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
+ brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
+ } else {
+ brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
+ }
+ if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
+ brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
+ } else {
+ brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
+ }
+ brokers[i].mapping[j].converterType = "json";
+ } else {
+ brokers[i].mapping[j].converterType = "custom";
+ }
+ }
+ }
+ if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
+ for(let j=0;j<brokers[i].connectRequests.length;j++) {
+ if(brokers[i].connectRequests[j].deviceNameTopicExpression) {
+ brokers[i].connectRequests[j].nameExp = "deviceNameTopicExpression";
+ } else {
+ brokers[i].connectRequests[j].nameExp = "deviceNameJsonExpression";
+ }
+ }
+ }
+ if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
+ for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
+ if(brokers[i].disconnectRequests[j].deviceNameTopicExpression) {
+ brokers[i].disconnectRequests[j].nameExp = "deviceNameTopicExpression";
+ } else {
+ brokers[i].disconnectRequests[j].nameExp = "deviceNameJsonExpression";
+ }
+ }
+ }
+ if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
+ for(let j=0;j<brokers[i].attributeRequests.length;j++) {
+ if(brokers[i].attributeRequests[j].deviceNameTopicExpression) {
+ brokers[i].attributeRequests[j].nameExp = "deviceNameTopicExpression";
+ } else {
+ brokers[i].attributeRequests[j].nameExp = "deviceNameJsonExpression";
+ }
+ if(brokers[i].attributeRequests[j].attributeKeyTopicExpression) {
+ brokers[i].attributeRequests[j].attrKey = "attributeKeyTopicExpression";
+ } else {
+ brokers[i].attributeRequests[j].attrKey = "attributeKeyJsonExpression";
+ }
+ if(brokers[i].attributeRequests[j].requestIdTopicExpression) {
+ brokers[i].attributeRequests[j].requestId = "requestIdTopicExpression";
+ } else {
+ brokers[i].attributeRequests[j].requestId = "requestIdJsonExpression";
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/*@ngInject*/
+export function ParseToNull() {
+ var linker = function (scope, elem, attrs, ngModel) {
+ ngModel.$parsers.push(function(value) {
+ if(value === "") {
+ return null;
+ }
+ return value;
+ })
+ };
+ return {
+ restrict: "A",
+ link: linker,
+ require: "ngModel"
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-dialog.tpl.html b/ui/src/app/extension/extension-dialog.tpl.html
new file mode 100644
index 0000000..73c33d8
--- /dev/null
+++ b/ui/src/app/extension/extension-dialog.tpl.html
@@ -0,0 +1,81 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
+ <form name="theForm" ng-submit="vm.save()" novalidate>
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
+
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <md-content class="md-padding" layout="column">
+ <fieldset ng-disabled="loading">
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm.extensionId.$touched && theForm.extensionId.$invalid">
+ <label translate>extension.extension-id</label>
+ <input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
+ <div ng-messages="theForm.extensionId.$error">
+ <div translate ng-message="required">extension.field-required</div>
+ <div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="40" class="md-block" md-is-error="theForm.extensionType.$touched && theForm.extensionType.$invalid">
+ <label translate>extension.extension-type</label>
+
+ <md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
+ <md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
+ {{value}}
+ </md-option>
+ </md-select>
+
+ <div ng-messages="theForm.extensionType.$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <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>
+ <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>
+ <div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
+ </fieldset>
+ </md-content>
+ </div>
+ </md-dialog-content>
+
+ <md-dialog-actions layout="row">
+ <md-button type="submit"
+ class="md-raised md-primary"
+ >
+ {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
+ </md-button>
+
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
+ </md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss
new file mode 100644
index 0000000..97ac717
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form.scss
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+.extension-form {
+ li > .md-button {
+ color: rgba(0, 0, 0, 0.7);
+ margin: 0;
+ }
+ .vAccordion--default {
+ margin-top: 0;
+ padding-left: 3px;
+ }
+ .tb-container {
+ width:100%;
+ }
+ .dropdown-messages {
+ .tb-error-message {
+ padding: 5px 0 0 0;
+ }
+ }
+ .dropdown-section {
+ margin-bottom: 30px;
+ }
+ v-pane.inner-invalid > v-pane-header {
+ border-bottom: 2px solid rgb(221,44,0);
+ }
+}
+
+.extension-form.extension-mqtt {
+ md-checkbox{
+ margin-left: 10px;
+ }
+}
+
+.tb-extension-custom-transformer-panel {
+ margin-left: 15px;
+ border: 1px solid #C0C0C0;
+ height: 100%;
+ .tb-extension-custom-transformer {
+ min-width: 600px;
+ min-height: 200px;
+ width: 100%;
+ height: 100%;
+ }
+ .ace_text-input {
+ position:absolute!important
+ }
+}
+
+.extensionDialog {
+ min-width: 1000px;
+}
+
+.tb-container-for-select {
+ height: 58px;
+}
+
+.tb-drop-file-input-hide {
+ height: 200%;
+ display: block;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
new file mode 100644
index 0000000..37a9444
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
@@ -0,0 +1,147 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/theme/github';
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormHttpTemplate from './extension-form-http.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
+
+ var linker = function(scope, element) {
+
+ var template = $templateCache.get(extensionFormHttpTemplate);
+ element.html(template);
+
+ scope.types = types;
+ scope.theForm = scope.$parent.theForm;
+
+ scope.extensionCustomTransformerOptions = {
+ useWrapMode: false,
+ mode: 'json',
+ showGutter: true,
+ showPrintMargin: true,
+ theme: 'github',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function(_ace) {
+ _ace.$blockScrolling = 1;
+ }
+ };
+
+
+ scope.addConverterConfig = function() {
+ var newConverterConfig = {converterId:"", converters:[]};
+ scope.converterConfigs.push(newConverterConfig);
+
+ scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
+ scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
+ };
+
+ scope.removeConverterConfig = function(config) {
+ var index = scope.converterConfigs.indexOf(config);
+ if (index > -1) {
+ scope.converterConfigs.splice(index, 1);
+ }
+ };
+
+ scope.addConverter = function(converters) {
+ var newConverter = {
+ deviceNameJsonExpression:"",
+ deviceTypeJsonExpression:"",
+ attributes:[],
+ timeseries:[]
+ };
+ converters.push(newConverter);
+ };
+
+ scope.removeConverter = function(converter, converters) {
+ var index = converters.indexOf(converter);
+ if (index > -1) {
+ converters.splice(index, 1);
+ }
+ };
+
+ scope.addAttribute = function(attributes) {
+ var newAttribute = {type:"", key:"", value:""};
+ attributes.push(newAttribute);
+ };
+
+ scope.removeAttribute = function(attribute, attributes) {
+ var index = attributes.indexOf(attribute);
+ if (index > -1) {
+ attributes.splice(index, 1);
+ }
+ };
+
+
+ if(scope.isAdd) {
+ scope.converterConfigs = scope.config.converterConfigurations;
+ scope.addConverterConfig();
+ } else {
+ scope.converterConfigs = scope.config.converterConfigurations;
+ }
+
+ scope.transformerTypeChange = function(attribute) {
+ attribute.transformer = "";
+ };
+
+ scope.validateTransformer = function (model, editorName) {
+ if(model && model.length) {
+ try {
+ angular.fromJson(model);
+ scope.theForm[editorName].$setValidity('transformerJSON', true);
+ } catch(e) {
+ scope.theForm[editorName].$setValidity('transformerJSON', false);
+ }
+ }
+ };
+
+ scope.collapseValidation = function(index, id) {
+ var invalidState = angular.element('#'+id+':has(.ng-invalid)');
+ if(invalidState.length) {
+ invalidState.addClass('inner-invalid');
+ }
+ };
+
+ scope.expandValidation = function (index, id) {
+ var invalidState = angular.element('#'+id);
+ invalidState.removeClass('inner-invalid');
+ };
+
+ $compile(element.contents())(scope);
+ };
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ config: "=",
+ isAdd: "="
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
new file mode 100644
index 0000000..b39794a
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
@@ -0,0 +1,311 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-card class="extension-form extension-http">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">extension.configuration</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-card-content>
+ <v-accordion id="http-converter-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="http-converters-pane" expanded="true">
+ <v-pane-header>
+ {{ 'extension.converter-configurations' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="converterConfigs.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
+ <md-button aria-label="{{ 'action.remove' | translate }}"
+ class="md-icon-button"
+ ng-click="removeConverterConfig(config)"
+ ng-hide="converterConfigs.length < 2"
+ >
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+
+ <md-input-container class="md-block" md-is-error="theForm['httpConverterId_' + configIndex].$touched && theForm['httpConverterId_' + configIndex].$invalid">
+ <label translate>extension.converter-id</label>
+ <input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
+ <div ng-messages="theForm['httpConverterId_' + configIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>extension.token</label>
+ <input name="httpToken" ng-model="config.token" parse-to-null>
+ </md-input-container>
+ <v-accordion id="http-converters-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="http-converters-pane_{{configIndex}}" expanded="true">
+ <v-pane-header>
+ {{ 'extension.converters' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="config.converters.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item"
+ ng-repeat="(converterIndex,converter) in config.converters"
+ >
+ <md-button aria-label="{{ 'action.remove' | translate }}"
+ class="md-icon-button"
+ ng-click="removeConverter(converter, config.converters)"
+ ng-hide="config.converters.length < 2"
+ >
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <md-input-container class="md-block" md-is-error="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceNameExp_' + configIndex + converterIndex].$invalid">
+ <label translate>extension.device-name-expression</label>
+ <input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
+ <div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block" md-is-error="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$invalid">
+ <label translate>extension.device-type-expression</label>
+ <input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
+ <div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <v-accordion id="http-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="http-attributes-pane_{{configIndex}}{{converterIndex}}">
+ <v-pane-header>
+ {{ 'extension.attributes' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="converter.attributes.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$invalid">
+ <label translate>extension.key</label>
+ <input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
+ <div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$invalid">
+ <label translate>extension.type</label>
+ <md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
+ <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+ {{attrTypeValue | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$invalid">
+ <label translate>extension.value</label>
+ <input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
+ <div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+
+ <md-input-container flex="40" class="md-block">
+ <label translate>extension.transformer</label>
+ <md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
+ <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ </section>
+
+ <div ng-if='attribute.transformerType == "custom"'>
+ <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+ <div flex class="tb-extension-custom-transformer-panel">
+ <div flex class="tb-extension-custom-transformer"
+ ui-ace="extensionCustomTransformerOptions"
+ ng-model="attribute.transformer"
+ name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
+ ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
+ required>
+ </div>
+ </div>
+ <div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
+ <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
+ <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
+ </div>
+ </div>
+
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-attribute</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+
+ <v-accordion id="http-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="http-timeseries-pane_{{configIndex}}{{converterIndex}}">
+ <v-pane-header>
+ {{ 'extension.timeseries' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="converter.timeseries.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$invalid">
+ <label translate>extension.key</label>
+ <input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
+ <div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$invalid">
+ <label translate>extension.type</label>
+ <md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
+ <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+ {{attrTypeValue | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$invalid">
+ <label translate>extension.value</label>
+ <input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+ <div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+
+ <md-input-container flex="40" class="md-block">
+ <label translate>extension.transformer</label>
+ <md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
+ <md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ </section>
+
+ <div ng-if='timeseries.transformerType == "custom"'>
+ <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+ <div flex class="tb-extension-custom-transformer-panel">
+ <div flex class="tb-extension-custom-transformer"
+ ui-ace="extensionCustomTransformerOptions"
+ ng-model="timeseries.transformer"
+ name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
+ ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
+ required>
+ </div>
+ </div>
+ <div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
+ <div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
+ <div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
+ </div>
+ </div>
+
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-timeseries</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-converter</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-config</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ <!--{{config}}-->
+ </md-card-content>
+</md-card>
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
new file mode 100644
index 0000000..39a1eb8
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
@@ -0,0 +1,347 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
+
+ var linker = function(scope, element) {
+
+ var template = $templateCache.get(extensionFormMqttTemplate);
+ element.html(template);
+
+ scope.types = types;
+ scope.theForm = scope.$parent.theForm;
+
+ scope.deviceNameExpressions = {
+ deviceNameJsonExpression: "extension.converter-json",
+ deviceNameTopicExpression: "extension.topic"
+ };
+ scope.deviceTypeExpressions = {
+ deviceTypeJsonExpression: "extension.converter-json",
+ deviceTypeTopicExpression: "extension.topic"
+ };
+ scope.attributeKeyExpressions = {
+ attributeKeyJsonExpression: "extension.converter-json",
+ attributeKeyTopicExpression: "extension.topic"
+ };
+ scope.requestIdExpressions = {
+ requestIdJsonExpression: "extension.converter-json",
+ requestIdTopicExpression: "extension.topic"
+ }
+
+ scope.extensionCustomConverterOptions = {
+ useWrapMode: false,
+ mode: 'json',
+ showGutter: true,
+ showPrintMargin: true,
+ theme: 'github',
+ advanced: {
+ enableSnippets: true,
+ enableBasicAutocompletion: true,
+ enableLiveAutocompletion: true
+ },
+ onLoad: function(_ace) {
+ _ace.$blockScrolling = 1;
+ }
+ };
+
+ scope.updateValidity = function () {
+ if(scope.brokers.length) {
+ for(let i=0;i<scope.brokers.length;i++) {
+ if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
+ if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
+ scope.theForm.$setValidity('cert.PEM', false);
+ break;
+ } else {
+ scope.theForm.$setValidity('cert.PEM', true);
+ }
+ }
+ }
+ }
+ };
+
+ scope.$watch('brokers', function() {
+ scope.updateValidity();
+ }, true);
+
+ scope.addBroker = function() {
+ var newBroker = {
+ host: "localhost",
+ port: 1882,
+ ssl: false,
+ retryInterval: 3000,
+ credentials: {type:"anonymous"},
+ mapping: [],
+ connectRequests: [],
+ disconnectRequests: [],
+ attributeRequests: [],
+ attributeUpdates: [],
+ serverSideRpc: []
+ };
+ scope.brokers.push(newBroker);
+ };
+
+ scope.removeBroker = function(broker) {
+ var index = scope.brokers.indexOf(broker);
+ if (index > -1) {
+ scope.brokers.splice(index, 1);
+ }
+ };
+
+ if(scope.isAdd) {
+ scope.brokers = [];
+ scope.config.brokers = scope.brokers;
+ scope.addBroker();
+ } else {
+ scope.brokers = scope.config.brokers;
+ }
+
+ scope.addMap = function(mapping) {
+ var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
+
+ mapping.push(newMap);
+ };
+
+ scope.removeMap = function(map, mapping) {
+ var index = mapping.indexOf(map);
+ if (index > -1) {
+ mapping.splice(index, 1);
+ }
+ };
+
+ scope.addAttribute = function(attributes) {
+ var newAttribute = {type:"", key:"", value:""};
+ attributes.push(newAttribute);
+ };
+
+ scope.removeAttribute = function(attribute, attributes) {
+ var index = attributes.indexOf(attribute);
+ if (index > -1) {
+ attributes.splice(index, 1);
+ }
+ };
+
+ scope.addConnectRequest = function(requests, type) {
+ var newRequest = {};
+ if(type == "connect") {
+ newRequest.topicFilter = "sensors/connect";
+ } else {
+ newRequest.topicFilter = "sensors/disconnect";
+ }
+ requests.push(newRequest);
+ };
+
+ scope.addAttributeRequest = function(requests) {
+ var newRequest = {
+ topicFilter: "sensors/attributes",
+ clientScope: false,
+ responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}",
+ valueExpression: "${attributeValue}"
+ };
+ requests.push(newRequest);
+ };
+
+ scope.addAttributeUpdate = function(updates) {
+ var newUpdate = {
+ deviceNameFilter: ".*",
+ attributeFilter: ".*",
+ topicExpression: "sensor/${deviceName}/${attributeKey}",
+ valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}"
+ }
+ updates.push(newUpdate);
+ };
+
+ scope.addServerSideRpc = function(rpcRequests) {
+ var newRpc = {
+ deviceNameFilter: ".*",
+ methodFilter: "echo",
+ requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}",
+ responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}",
+ responseTimeout: 10000,
+ valueExpression: "${params}"
+ };
+ rpcRequests.push(newRpc);
+ };
+
+ scope.changeCredentials = function(broker) {
+ var type = broker.credentials.type;
+ broker.credentials = {};
+ broker.credentials.type = type;
+ };
+
+ scope.changeConverterType = function(map) {
+ if(map.converterType == "custom"){
+ map.converter = "";
+ }
+ if(map.converterType == "json") {
+ map.converter = {attributes:[],timeseries:[]};
+ }
+ };
+
+ scope.changeNameExpression = function(element, type) {
+ if(element.nameExp == "deviceNameJsonExpression") {
+ if(element.deviceNameTopicExpression) {
+ delete element.deviceNameTopicExpression;
+ }
+ if(type) {
+ element.deviceNameJsonExpression = "${$.serialNumber}";
+ }
+ }
+ if(element.nameExp == "deviceNameTopicExpression") {
+ if(element.deviceNameJsonExpression) {
+ delete element.deviceNameJsonExpression;
+ }
+ if(type && type == "connect") {
+ element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)";
+ }
+ if(type && type == "disconnect") {
+ element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)";
+ }
+ if(type && type == "attribute") {
+ element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)";
+ }
+ }
+ };
+
+ scope.changeTypeExpression = function(converter) {
+ if(converter.typeExp == "deviceTypeJsonExpression") {
+ if(converter.deviceTypeTopicExpression) {
+ delete converter.deviceTypeTopicExpression;
+ }
+ }
+ if(converter.typeExp == "deviceTypeTopicExpression") {
+ if(converter.deviceTypeJsonExpression) {
+ delete converter.deviceTypeJsonExpression;
+ }
+ }
+ };
+
+ scope.changeAttrKeyExpression = function(request) {
+ if(request.attrKey == "attributeKeyJsonExpression") {
+ if(request.attributeKeyTopicExpression) {
+ delete request.attributeKeyTopicExpression;
+ }
+ request.attributeKeyJsonExpression = "${$.key}";
+ }
+ if(request.attrKey == "attributeKeyTopicExpression") {
+ if(request.attributeKeyJsonExpression) {
+ delete request.attributeKeyJsonExpression;
+ }
+ request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)";
+ }
+ };
+
+ scope.changeRequestIdExpression = function(request) {
+ if(request.requestId == "requestIdJsonExpression") {
+ if(request.requestIdTopicExpression) {
+ delete request.requestIdTopicExpression;
+ }
+ request.requestIdJsonExpression = "${$.requestId}";
+ }
+ if(request.requestId == "requestIdTopicExpression") {
+ if(request.requestIdJsonExpression) {
+ delete request.requestIdJsonExpression;
+ }
+ request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)";
+ }
+ };
+
+ scope.validateCustomConverter = function(model, editorName) {
+ if(model && model.length) {
+ try {
+ angular.fromJson(model);
+ scope.theForm[editorName].$setValidity('converterJSON', true);
+ } catch(e) {
+ scope.theForm[editorName].$setValidity('converterJSON', false);
+ }
+ }
+ };
+
+ scope.fileAdded = function($file, broker, fileType) {
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ scope.$apply(function() {
+ if(event.target.result) {
+ scope.theForm.$setDirty();
+ var addedFile = event.target.result;
+ if (addedFile && addedFile.length > 0) {
+ if(fileType == "caCert") {
+ broker.credentials.caCertFileName = $file.name;
+ broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
+ }
+ if(fileType == "privateKey") {
+ broker.credentials.privateKeyFileName = $file.name;
+ broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
+ }
+ if(fileType == "Cert") {
+ broker.credentials.certFileName = $file.name;
+ broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
+ }
+ }
+ }
+ });
+ };
+ reader.readAsDataURL($file.file);
+ };
+
+ scope.clearFile = function(broker, fileType) {
+ scope.theForm.$setDirty();
+ if(fileType == "caCert") {
+ broker.credentials.caCertFileName = null;
+ broker.credentials.caCert = null;
+ }
+ if(fileType == "privateKey") {
+ broker.credentials.privateKeyFileName = null;
+ broker.credentials.privateKey = null;
+ }
+ if(fileType == "Cert") {
+ broker.credentials.certFileName = null;
+ broker.credentials.cert = null;
+ }
+ };
+
+ scope.collapseValidation = function(index, id) {
+ var invalidState = angular.element('#'+id+':has(.ng-invalid)');
+ if(invalidState.length) {
+ invalidState.addClass('inner-invalid');
+ }
+ };
+
+ scope.expandValidation = function (index, id) {
+ var invalidState = angular.element('#'+id);
+ invalidState.removeClass('inner-invalid');
+ };
+
+ $compile(element.contents())(scope);
+ };
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ config: "=",
+ isAdd: "="
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
new file mode 100644
index 0000000..f665fb0
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
@@ -0,0 +1,860 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-card class="extension-form extension-mqtt">
+ <md-card-title name="testValid">
+ <md-card-title-text>
+ <span translate class="md-headline">extension.configuration</span>
+ </md-card-title-text>
+ </md-card-title>
+ <md-card-content>
+ <v-accordion id="mqtt-brokers-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-brokers-pane" expanded="true">
+ <v-pane-header>
+ {{ 'extension.brokers' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="brokers.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(brokerIndex,broker) in brokers">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeBroker(broker)" ng-hide="brokers.length < 2">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block">
+ <label translate>extension.port</label>
+ <input required type="number" min="1" max="65535" name="mqttPort_{{brokerIndex}}" ng-model="broker.port">
+ <div ng-messages="theForm['mqttPort_' + brokerIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ <div translate ng-message="min">extension.port-range</div>
+ <div translate ng-message="max">extension.port-range</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="60" class="md-block">
+ <label translate>extension.host</label>
+ <input required name="mqttHost_{{brokerIndex}}" ng-model="broker.host">
+ <div ng-messages="theForm['mqttHost_' + brokerIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block">
+ <label translate>extension.retry-interval</label>
+ <input required type="number" name="mqttRetryInterval_{{brokerIndex}}" ng-model="broker.retryInterval">
+ <div ng-messages="theForm['mqttRetryInterval_' + brokerIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.credentials</label>
+ <md-select required name="mqttCredentials_{{brokerIndex}}" ng-model="broker.credentials.type" ng-change="changeCredentials(broker)">
+ <md-option ng-repeat="(credentialsType, credentialsValue) in types.mqttCredentialTypes" ng-value="credentialsValue.value">
+ {{credentialsValue.name | translate}}
+ </md-option>
+ </md-select>
+ </md-input-container>
+ <md-input-container flex="10" class="md-block">
+ <md-checkbox flex aria-label="{{ 'extension.ssl' | translate }}"
+ ng-model="broker.ssl">{{ 'extension.ssl' | translate }}
+ </md-checkbox>
+ </md-input-container>
+ </section>
+ <section flex layout="row" ng-if='broker.credentials.type == "basic"'>
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttUsername_' + brokerIndex].$touched && theForm['mqttUsername_' + brokerIndex].$invalid">
+ <label translate>extension.username</label>
+ <input required name="mqttUsername_{{brokerIndex}}" ng-model="broker.credentials.username">
+ <div ng-messages="theForm['mqttUsername_' + brokerIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttPassword_' + brokerIndex].$touched && theForm['mqttPassword_' + brokerIndex].$invalid">
+ <label translate>extension.password</label>
+ <input required name="mqttPassword_{{brokerIndex}}" ng-model="broker.credentials.password">
+ <div ng-messages="theForm['mqttPassword_' + brokerIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="column" ng-if='broker.credentials.type == "cert.PEM"' class="dropdown-section">
+ <div class="tb-container" ng-class="broker.credentials.caCertFileName ? 'ng-valid' : 'ng-invalid'">
+ <label class="tb-label" translate>extension.ca-cert</label>
+ <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "caCert")' class="tb-file-select-container">
+ <div class="tb-file-clear-container">
+ <md-button ng-click='clearFile(broker, "caCert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="caCertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="caCertSelect_{{brokerIndex}}">
+ </div>
+ </div>
+ </div>
+ <div class="dropdown-messages">
+ <div ng-if="!broker.credentials.caCertFileName" class="tb-error-message" translate>extension.no-file</div>
+ <div ng-if="broker.credentials.caCertFileName">{{broker.credentials.caCertFileName}}</div>
+ </div>
+ <div class="tb-container" ng-class="broker.credentials.privateKeyFileName ? 'ng-valid' : 'ng-invalid'">
+ <label class="tb-label" translate>extension.private-key</label>
+ <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "privateKey")' class="tb-file-select-container">
+ <div class="tb-file-clear-container">
+ <md-button ng-click='clearFile(broker, "privateKey")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="privateKeySelect_{{brokerIndex}}" translate>extension.drop-file</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="privateKeySelect_{{brokerIndex}}">
+ </div>
+ </div>
+ </div>
+ <div class="dropdown-messages">
+ <div ng-if="!broker.credentials.privateKeyFileName" class="tb-error-message" translate>extension.no-file</div>
+ <div ng-if="broker.credentials.privateKeyFileName">{{broker.credentials.privateKeyFileName}}</div>
+ </div>
+ <div class="tb-container" ng-class="broker.credentials.certFileName ? 'ng-valid' : 'ng-invalid'">
+ <label class="tb-label" translate>extension.cert</label>
+ <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "Cert")' class="tb-file-select-container">
+ <div class="tb-file-clear-container">
+ <md-button ng-click='clearFile(broker, "Cert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="CertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="CertSelect_{{brokerIndex}}">
+ </div>
+ </div>
+ </div>
+ <div class="dropdown-messages">
+ <div ng-if="!broker.credentials.certFileName" class="tb-error-message" translate>extension.no-file</div>
+ <div ng-if="broker.credentials.certFileName">{{broker.credentials.certFileName}}</div>
+ </div>
+ </section>
+
+ <v-accordion id="mqtt-mapping-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-mapping-pane_{{brokerIndex}}">
+ <v-pane-header>
+ {{ 'extension.mapping' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="broker.mapping.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(mapIndex,map) in broker.mapping">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeMap(map, broker.mapping)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttConverterType_' + brokerIndex + mapIndex].$touched && theForm['mqttConverterType_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.converter-type</label>
+ <md-select required name="mqttConverterType_{{brokerIndex}}{{mapIndex}}" ng-model="map.converterType" ng-change="changeConverterType(map)">
+ <md-option ng-repeat="(converterType, value) in types.mqttConverterTypes" ng-value="converterType">
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="60" class="md-block">
+ <label translate>extension.topic-filter</label>
+ <input required name="mqttTopicFilter_{{brokerIndex}}{{mapIndex}}" ng-model="map.topicFilter">
+ <div ng-messages="theForm['mqttTopicFilter_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+
+ <div ng-if='map.converterType =="json"' ng-init="map.converter.type = 'json'">
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.device-name-expression</label>
+ <md-select required name="mqttDeviceNameExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.nameExp" ng-change="changeNameExpression(map.converter)">
+ <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="map.converter.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.json-name-expression</label>
+ <input required name="mqttJsonNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameJsonExpression">
+ <div ng-messages="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="map.converter.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.topic-name-expression</label>
+ <input required name="mqttTopicNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameTopicExpression">
+ <div ng-messages="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.device-type-expression</label>
+ <md-select required name="mqttDeviceTypeExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.typeExp" ng-change="changeTypeExpression(map.converter)">
+ <md-option ng-repeat="(key, value) in deviceTypeExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="map.converter.typeExp == 'deviceTypeJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.json-type-expression</label>
+ <input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
+ <div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="map.converter.typeExp == 'deviceTypeTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.topic-type-expression</label>
+ <input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
+ <div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block">
+ <label translate>extension.timeout</label>
+ <input type="number" name="mqttTimeout_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.timeout" parse-to-null>
+ </md-input-container>
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
+ <label translate>extension.filter-expression</label>
+ <input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
+ <div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </div>
+
+ <div ng-if='map.converterType == "custom"'>
+ <div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
+ <div flex class="tb-extension-custom-transformer-panel">
+ <div flex class="tb-extension-custom-transformer"
+ ui-ace="extensionCustomConverterOptions"
+ ng-model="map.converter"
+ name="mqttCustomConverter_{{brokerIndex}}{{mapIndex}}"
+ ng-change='validateCustomConverter(map.converter, "mqttCustomConverter_" + brokerIndex + mapIndex)'
+ required>
+ </div>
+ </div>
+ <div class="tb-error-messages" ng-messages="theForm['mqttCustomConverter_' + brokerIndex + mapIndex].$error" role="alert">
+ <div ng-message="required" class="tb-error-message" translate>extension.converter-json-required</div>
+ <div ng-message="converterJSON" class="tb-error-message" translate>extension.converter-json-parse</div>
+ </div>
+ </div>
+
+ <v-accordion ng-if='map.converterType =="json"' id="mqtt-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-attributes-pane_{{brokerIndex}}{{mapIndex}}">
+ <v-pane-header>
+ {{ 'extension.attributes' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="map.converter.attributes.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(attributeIndex, attribute) in map.converter.attributes">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, map.converter.attributes)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$invalid">
+ <label translate>extension.key</label>
+ <input required name="mqttAttributeKey_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.key">
+ <div ng-messages="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$invalid">
+ <label translate>extension.type</label>
+ <md-select required name="mqttAttributeType_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.type">
+ <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+ {{attrTypeValue | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <md-input-container class="md-block" md-is-error="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$invalid">
+ <label translate>extension.value</label>
+ <input required name="mqttAttributeValue_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.value">
+ <div ng-messages="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addAttribute(map.converter.attributes)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-attribute</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion ng-if='map.converterType =="json"' id="mqtt-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-timeseries-pane_{{brokerIndex}}{{mapIndex}}">
+ <v-pane-header>
+ {{ 'extension.timeseries' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="map.converter.timeseries.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in map.converter.timeseries">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, map.converter.timeseries)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block" md-is-error="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
+ <label translate>extension.key</label>
+ <input required name="mqttTimeseriesKey_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
+ <div ng-messages="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
+ <label translate>extension.type</label>
+ <md-select required name="mqttTimeseriesType_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
+ <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
+ {{attrTypeValue | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <md-input-container class="md-block" md-is-error="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
+ <label translate>extension.value</label>
+ <input required name="mqttTimeseriesValue_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+ <div ng-messages="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addAttribute(map.converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-timeseries</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addMap(broker.mapping)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-map</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion id="mqtt-connect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-connect-requests-pane_{{brokerIndex}}">
+ <v-pane-header>
+ {{ 'extension.connect-requests' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="broker.connectRequests.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(connectRequestIndex, connectRequest) in broker.connectRequests">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(connectRequest, broker.connectRequests)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <md-input-container class="md-block">
+ <label translate>extension.topic-filter</label>
+ <input required name="conRequestTopicFilter_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.topicFilter">
+ <div ng-messages="theForm['conRequestTopicFilter_' + brokerIndex + connectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$touched && theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$invalid">
+ <label translate>extension.device-name-expression</label>
+ <md-select required name="connectDeviceNameExpression_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.nameExp" ng-change="changeNameExpression(connectRequest, 'connect')">
+ <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="connectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+ <label translate>extension.json-name-expression</label>
+ <input required name="connectJsonNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameJsonExpression">
+ <div ng-messages="theForm['connectJsonNameExp_' + brokerIndex + connectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="connectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+ <label translate>extension.topic-name-expression</label>
+ <input required name="connectTopicNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameTopicExpression">
+ <div ng-messages="theForm['connectTopicNameExp_' + brokerIndex + connectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click='addConnectRequest(broker.connectRequests, "connect")' aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-connect-request</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion id="mqtt-disconnect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-disconnect-requests-pane_{{brokerIndex}}">
+ <v-pane-header>
+ {{ 'extension.disconnect-requests' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="broker.disconnectRequests.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(disconnectRequestIndex, disconnectRequest) in broker.disconnectRequests">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(disconnectRequest, broker.disconnectRequests)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <md-input-container class="md-block">
+ <label translate>extension.topic-filter</label>
+ <input required name="disconRequestTopicFilter_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.topicFilter">
+ <div ng-messages="theForm['disconRequestTopicFilter_' + brokerIndex + disconnectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$touched && theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$invalid">
+ <label translate>extension.device-name-expression</label>
+ <md-select required name="disconnectDeviceNameExpression_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.nameExp" ng-change="changeNameExpression(disconnectRequest, 'disconnect')">
+ <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+ <label translate>extension.json-name-expression</label>
+ <input required name="disconnectJsonNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameJsonExpression">
+ <div ng-messages="theForm['disconnectJsonNameExp_' + brokerIndex + disconnectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+ <label translate>extension.topic-name-expression</label>
+ <input required name="disconnectTopicNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameTopicExpression">
+ <div ng-messages="theForm['disconnectTopicNameExp_' + brokerIndex + disconnectRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click='addConnectRequest(broker.disconnectRequests, "disconnect")' aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-disconnect-request</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion id="mqtt-attribute-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-attribute-requests-pane_{{brokerIndex}}">
+ <v-pane-header>
+ {{ 'extension.attribute-requests' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="broker.attributeRequests.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(attributeRequestIndex, attributeRequest) in broker.attributeRequests">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeRequest, broker.attributeRequests)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="80" class="md-block">
+ <label translate>extension.topic-filter</label>
+ <input required name="attributeRequestTopicFilter_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.topicFilter">
+ <div ng-messages="theForm['attributeRequestTopicFilter_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="20" class="md-block">
+ <md-checkbox flex aria-label="{{ 'extension.client-scope' | translate }}"
+ ng-model="attributeRequest.clientScope">{{ 'extension.client-scope' | translate }}
+ </md-checkbox>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$invalid">
+ <label translate>extension.device-name-expression</label>
+ <md-select required name="attrRequestDeviceNameExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.nameExp" ng-change="changeNameExpression(attributeRequest, 'attribute')">
+ <md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="attributeRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
+ <label translate>extension.json-name-expression</label>
+ <input required name="attrRequestJsonNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameJsonExpression">
+ <div ng-messages="theForm['attrRequestJsonNameExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="attributeRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
+ <label translate>extension.topic-name-expression</label>
+ <input required name="attrRequestTopicNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameTopicExpression">
+ <div ng-messages="theForm['attrRequestTopicNameExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$invalid">
+ <label translate>extension.attribute-key-expression</label>
+ <md-select required name="attrRequestAttributeKeyExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attrKey" ng-change="changeAttrKeyExpression(attributeRequest)">
+ <md-option ng-repeat="(key, value) in attributeKeyExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyJsonExpression'" flex="60" class="md-block">
+ <label translate>extension.attr-json-key-expression</label>
+ <input required name="attrRequestJsonKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyJsonExpression">
+ <div ng-messages="theForm['attrRequestJsonKeyExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyTopicExpression'" flex="60" class="md-block">
+ <label translate>extension.attr-topic-key-expression</label>
+ <input required name="attrRequestTopicKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyTopicExpression">
+ <div ng-messages="theForm['attrRequestTopicKeyExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+
+ <section flex layout="row">
+ <md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$invalid">
+ <label translate>extension.request-id-expression</label>
+ <md-select required name="attrRequestIdExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestId" ng-change="changeRequestIdExpression(attributeRequest)">
+ <md-option ng-repeat="(key, value) in requestIdExpressions" ng-value='key'>
+ {{value | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="attributeRequest.requestId == 'requestIdJsonExpression'" flex="60" class="md-block">
+ <label translate>extension.request-id-json-expression</label>
+ <input required name="attrRequestJsonIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdJsonExpression">
+ <div ng-messages="theForm['attrRequestJsonIdExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container ng-if="attributeRequest.requestId == 'requestIdTopicExpression'" flex="60" class="md-block">
+ <label translate>extension.request-id-topic-expression</label>
+ <input required name="attrRequestTopicIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdTopicExpression">
+ <div ng-messages="theForm['attrRequestTopicIdExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+
+ <md-input-container class="md-block">
+ <label translate>extension.response-topic-expression</label>
+ <input required name="attributeRequestResponseTopicExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.responseTopicExpression">
+ <div ng-messages="theForm['attributeRequestResponseTopicExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>extension.value-expression</label>
+ <input required name="attributeRequestValueExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.valueExpression">
+ <div ng-messages="theForm['attributeRequestValueExp_' + brokerIndex + attributeRequestIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addAttributeRequest(broker.attributeRequests)" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-attribute-request</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion id="mqtt-attribute-updates-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-attribute-updates-pane_{{brokerIndex}}">
+ <v-pane-header>
+ {{ 'extension.attribute-updates' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="broker.attributeUpdates.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(attributeUpdateIndex, attributeUpdate) in broker.attributeUpdates">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeUpdate, broker.attributeUpdates)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.device-name-filter</label>
+ <input required name="attributeUpdateDeviceNameFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.deviceNameFilter">
+ <div ng-messages="theForm['attributeUpdateDeviceNameFilter_' + brokerIndex + attributeUpdateIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.attribute-filter</label>
+ <input required name="attributeUpdateAttributeFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.attributeFilter">
+ <div ng-messages="theForm['attributeUpdateAttributeFilter_' + brokerIndex + attributeUpdateIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <md-input-container class="md-block">
+ <label translate>extension.topic-expression</label>
+ <input required name="attributeUpdateTopicExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.topicExpression">
+ <div ng-messages="theForm['attributeUpdateTopicExp_' + brokerIndex + attributeUpdateIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>extension.value-expression</label>
+ <input required name="attributeUpdateValueExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.valueExpression">
+ <div ng-messages="theForm['attributeUpdateValueExp_' + brokerIndex + attributeUpdateIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click='addAttributeUpdate(broker.attributeUpdates)' aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-attribute-update</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion id="mqtt-server-side-rpc-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="mqtt-server-side-rpc-pane_{{brokerIndex}}">
+ <v-pane-header>
+ {{ 'extension.server-side-rpc' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="broker.serverSideRpc.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(rpcIndex, rpc) in broker.serverSideRpc">
+ <md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(rpc, broker.serverSideRpc)">
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.device-name-filter</label>
+ <input required name="serverSideRpcDeviceNameFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.deviceNameFilter">
+ <div ng-messages="theForm['serverSideRpcDeviceNameFilter_' + brokerIndex + rpcIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.method-filter</label>
+ <input required name="serverSideRpcMethodFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.methodFilter">
+ <div ng-messages="theForm['serverSideRpcMethodFilter_' + brokerIndex + rpcIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <md-input-container class="md-block">
+ <label translate>extension.request-topic-expression</label>
+ <input required name="serverSideRpcRequestTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.requestTopicExpression">
+ <div ng-messages="theForm['serverSideRpcRequestTopicExp_' + brokerIndex + rpcIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container class="md-block">
+ <label translate>extension.response-topic-expression</label>
+ <input name="serverSideRpcResponseTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTopicExpression" parse-to-null>
+ </md-input-container>
+ <section flex layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.response-timeout</label>
+ <input type="number" name="serverSideRpcResponseTimeout_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTimeout" parse-to-null>
+ </md-input-container>
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.value-expression</label>
+ <input required name="serverSideRpcValueExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.valueExpression">
+ <div ng-messages="theForm['serverSideRpcValueExp_' + brokerIndex + rpcIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click='addServerSideRpc(broker.serverSideRpc)' aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-server-side-rpc-request</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-broker</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+<!--<pre>
+{{config | json}}
+</pre>-->
+ </md-card-content>
+</md-card>
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
new file mode 100644
index 0000000..fc69b68
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
@@ -0,0 +1,173 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'brace/ext/language_tools';
+import 'brace/mode/json';
+import 'brace/theme/github';
+
+import './extension-form.scss';
+
+/* eslint-disable angular/log */
+
+import extensionFormOpcTemplate from './extension-form-opc.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+/*@ngInject*/
+export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) {
+
+
+ var linker = function(scope, element) {
+
+
+ function Server() {
+ this.applicationName = "Thingsboard OPC-UA client";
+ this.applicationUri = "";
+ this.host = "localhost";
+ this.port = 49320;
+ this.scanPeriodInSeconds = 10;
+ this.timeoutInMillis = 5000;
+ this.security = "Basic128Rsa15";
+ this.identity = {
+ "type": "anonymous"
+ };
+ this.keystore = {
+ "type": "PKCS12",
+ "location": "example.pfx",
+ "password": "secret",
+ "alias": "gateway",
+ "keyPassword": "secret"
+ };
+ this.mapping = []
+ }
+
+ function Map() {
+ this.deviceNodePattern = "Channel1\\.Device\\d+$";
+ this.deviceNamePattern = "Device ${_System._DeviceId}";
+ this.attributes = [];
+ this.timeseries = [];
+ }
+
+ function Attribute() {
+ this.key = "Tag1";
+ this.type = "string";
+ this.value = "${Tag1}";
+ }
+
+ function Timeseries() {
+ this.key = "Tag2";
+ this.type = "long";
+ this.value = "${Tag2}";
+ }
+
+
+ var template = $templateCache.get(extensionFormOpcTemplate);
+ element.html(template);
+
+ scope.types = types;
+ scope.theForm = scope.$parent.theForm;
+
+
+ if (!scope.configuration.servers.length) {
+ scope.configuration.servers.push(new Server());
+ }
+
+ scope.addServer = function(serversList) {
+ serversList.push(new Server());
+ // scope.addMap(serversList[serversList.length-1].mapping);
+
+ scope.theForm.$setDirty();
+ };
+
+ scope.addMap = function(mappingList) {
+ mappingList.push(new Map());
+ scope.theForm.$setDirty();
+ };
+
+ scope.addNewAttribute = function(attributesList) {
+ attributesList.push(new Attribute());
+ scope.theForm.$setDirty();
+ };
+
+ scope.addNewTimeseries = function(timeseriesList) {
+ timeseriesList.push(new Timeseries());
+ scope.theForm.$setDirty();
+ };
+
+
+ scope.removeItem = (item, itemList) => {
+ var index = itemList.indexOf(item);
+ if (index > -1) {
+ itemList.splice(index, 1);
+ }
+ scope.theForm.$setDirty();
+ };
+
+
+ $compile(element.contents())(scope);
+
+
+ scope.fileAdded = function($file, model, options) {
+ let reader = new FileReader();
+ reader.onload = function(event) {
+ scope.$apply(function() {
+ if(event.target.result) {
+ scope.theForm.$setDirty();
+ let addedFile = event.target.result;
+
+ if (addedFile && addedFile.length > 0) {
+ model[options.fileName] = $file.name;
+ model[options.file] = addedFile.replace(/^data.*base64,/, "");
+
+ }
+ }
+ });
+ };
+ reader.readAsDataURL($file.file);
+
+ };
+
+ scope.clearFile = function(model, options) {
+ scope.theForm.$setDirty();
+
+ model[options.fileName] = null;
+ model[options.file] = null;
+
+ };
+
+ scope.collapseValidation = function(index, id) {
+ var invalidState = angular.element('#'+id+':has(.ng-invalid)');
+ if(invalidState.length) {
+ invalidState.addClass('inner-invalid');
+ }
+ };
+
+ scope.expandValidation = function (index, id) {
+ var invalidState = angular.element('#'+id);
+ invalidState.removeClass('inner-invalid');
+ };
+
+ };
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ configuration: "=",
+ isAdd: "="
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
new file mode 100644
index 0000000..6d74cc5
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
@@ -0,0 +1,557 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-card class="extension-form extension-opc">
+ <md-card-title>
+ <md-card-title-text>
+ <span translate class="md-headline">extension.configuration</span>
+ </md-card-title-text>
+ </md-card-title>
+
+ <md-card-content>
+ <v-accordion id="opc-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="opc-servers-pane" expanded="true">
+ <v-pane-header>
+ {{ 'extension.opc-server' | translate }}
+ </v-pane-header>
+
+ <v-pane-content>
+ <div ng-if="configuration.servers.length === 0">
+ <span translate layout-align="center center" class="tb-prompt">extension.opc-add-server-prompt</span>
+ </div>
+
+ <div ng-if="configuration.servers.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
+ <md-button aria-label="{{ 'action.remove' | translate }}"
+ class="md-icon-button"
+ ng-click="removeItem(server, configuration.servers)"
+ ng-hide="configuration.servers.length < 2"
+ >
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+
+ <md-card>
+ <md-card-content>
+
+ <div layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-application-name</label>
+ <input required name="applicationName_{{serverIndex}}" ng-model="server.applicationName">
+ <div ng-messages="theForm['applicationName_' + serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+
+ <md-input-container flex="50" class="md-block" md-is-error="theForm['applicationUri_' + serverIndex].$touched && theForm['applicationUri_' + serverIndex].$invalid">
+ <label translate>extension.opc-application-uri</label>
+ <input required name="applicationUri_{{serverIndex}}" ng-model="server.applicationUri">
+ <div ng-messages="theForm['applicationUri_' + serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+
+ <div layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.host</label>
+ <input required name="host_{{serverIndex}}" ng-model="server.host">
+ <div ng-messages="theForm['host_' + serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.port</label>
+ <input type="number"
+ required
+ name="port_{{serverIndex}}"
+ ng-model="server.port"
+ min="1"
+ max="65535"
+ >
+ <div ng-messages="theForm['port_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ <div translate
+ ng-message="min"
+ >extension.port-range</div>
+ <div translate
+ ng-message="max"
+ >extension.port-range</div>
+ </div>
+ </md-input-container>
+ </div>
+
+ <div layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-scan-period-in-seconds</label>
+ <input type="number"
+ required
+ name="scanPeriodInSeconds_{{serverIndex}}"
+ ng-model="server.scanPeriodInSeconds">
+ <div ng-messages="theForm['scanPeriodInSeconds_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.timeout</label>
+ <input type="number"
+ required name="timeoutInMillis_{{serverIndex}}"
+ ng-model="server.timeoutInMillis"
+ >
+ <div ng-messages="theForm['timeoutInMillis_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+ <div layout="row">
+
+ <md-input-container flex="50" class="md-block tb-container-for-select">
+ <label translate>extension.opc-security</label>
+ <md-select required
+ name="securityType_{{serverIndex}}"
+ ng-model="server.security">
+ <md-option ng-value="securityType"
+ ng-repeat="(securityType, securityValue) in types.extensionOpcSecurityTypes"
+ ><span ng-bind="::securityValue"></span></md-option>
+ </md-select>
+ <div ng-messages="theForm['securityType_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block tb-container-for-select">
+ <label translate>extension.opc-identity</label>
+ <md-select required
+ name="identityType_{{serverIndex}}"
+ ng-model="server.identity.type"
+ >
+ <md-option ng-value="identityType"
+ ng-repeat="(identityType, identityValue) in types.extensionIdentityType"
+ ><span ng-bind="identityValue | translate"></span></md-option>
+ </md-select>
+ <div ng-messages="theForm['identityType_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+ <div ng-if="server.identity.type != 'username'">
+ <span class=""
+ ng-init="server.identity = {'type':'anonymous'}"></span>
+ </div>
+ <div layout="row" ng-if="server.identity.type == 'username'">
+ <md-input-container flex="50" class="md-block" md-is-error="theForm['identityUsername_' + serverIndex].$touched && theForm['identityUsername_' + serverIndex].$invalid">
+ <label translate>extension.username</label>
+ <input required
+ name="identityUsername_{{serverIndex}}"
+ ng-model="server.identity.username"
+ >
+ <div ng-messages="theForm['identityUsername_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block" md-is-error="theForm['identityPassword_' + serverIndex].$touched && theForm['identityPassword_' + serverIndex].$invalid">
+ <label translate>extension.password</label>
+ <input required
+ name="identityPassword_{{serverIndex}}" ng-model="server.identity.password">
+ <div ng-messages="theForm['identityPassword_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+ <v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
+ <v-pane-header>
+ {{ 'extension.opc-keystore' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+
+ <md-input-container class="md-block tb-container-for-select">
+ <label translate>extension.opc-keystore-type</label>
+ <md-select required name="keystoreType_{{serverIndex}}" ng-model="server.keystore.type">
+ <md-option ng-value="keystoreType" ng-repeat="(keystoreType, keystoreValue) in types.extensionKeystoreType"><span ng-bind="::keystoreValue"></span></md-option>
+ </md-select>
+ <div ng-messages="theForm['keystoreType_'+serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <section class="dropdown-section">
+ <div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
+ <span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
+ <label class="tb-label" translate>extension.opc-keystore-location</label>
+ <div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
+ <div class="tb-file-clear-container">
+ <md-button ng-click='clearFile(server.keystore, fieldsToFill)' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="dropFileKeystore_{{serverIndex}}" translate>extension.drop-file</label>
+ <input flow-attrs="{accept:'.pfx,.p12'}"
+ type="file"
+ class="file-input"
+ flow-btn id="dropFileKeystore_{{serverIndex}}"
+ name="keystoreFile"
+ ng-model="server.keystore.file"
+ >
+ </div>
+ </div>
+ </div>
+ <div class="dropdown-messages">
+ <div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
+ <div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
+ </div>
+ </section>
+
+
+ <div flex layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-keystore-password</label>
+ <input required name="keystorePassword_{{serverIndex}}" ng-model="server.keystore.password">
+ <div ng-messages="theForm['keystorePassword_' + serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-keystore-alias</label>
+ <input required name="keystoreAlias_{{serverIndex}}" ng-model="server.keystore.alias">
+ <div ng-messages="theForm['keystoreAlias_' + serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+ <md-input-container class="md-block">
+ <label translate>extension.opc-keystore-key-password</label>
+ <input required name="keystoreKeyPassword_{{serverIndex}}" ng-model="server.keystore.keyPassword">
+ <div ng-messages="theForm['keystoreKeyPassword_' + serverIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+
+ <v-accordion id="opc-mapping-accordion"
+ class="vAccordion--default"
+ onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="opc-mapping-pane_{{serverIndex}}">
+ <v-pane-header>
+ {{ 'extension.mapping' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-if="server.mapping.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item"
+ ng-repeat="(mapIndex, map) in server.mapping"
+ >
+ <md-button aria-label="{{ 'action.remove' | translate }}"
+ class="md-icon-button"
+ ng-click="removeItem(map, server.mapping)"
+ >
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+
+ <md-card>
+ <md-card-content>
+ <div flex layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-device-node-pattern</label>
+ <input required
+ name="deviceNodePattern_{{serverIndex}}{{mapIndex}}"
+ ng-model="map.deviceNodePattern"
+ >
+ <div ng-messages="theForm['deviceNodePattern_' + serverIndex + mapIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-device-name-pattern</label>
+ <input required
+ name="deviceNamePattern_{{serverIndex}}{{mapIndex}}"
+ ng-model="map.deviceNamePattern"
+ >
+ <div ng-messages="theForm['deviceNamePattern_' + serverIndex + mapIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+
+ <v-accordion id="opc-attributes-accordion"
+ class="vAccordion--default"
+ onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="opc-attributes-pane_{{serverIndex}}{{mapIndex}}">
+ <v-pane-header>
+ {{ 'extension.attributes' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-show="map.attributes.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item"
+ ng-repeat="(attributeIndex, attribute) in map.attributes"
+ >
+ <md-button aria-label="{{ 'action.remove' | translate }}"
+ class="md-icon-button"
+ ng-click="removeItem(attribute, map.attributes)">
+ <ng-md-icon icon="close"
+ aria-label="{{ 'action.remove' | translate }}"
+ ></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+
+ <section flex
+ layout="row"
+ >
+ <md-input-container flex="60" class="md-block">
+ <label translate>extension.key</label>
+ <input required
+ name="opcAttributeKey_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+ ng-model="attribute.key"
+ >
+ <div ng-messages="theForm['opcAttributeKey_' + serverIndex + mapIndex + attributeIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="40" class="md-block tb-container-for-select">
+ <label translate>extension.type</label>
+ <md-select required name="opcAttributeType_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+ ng-model="attribute.type"
+ >
+ <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
+ ng-value="attrType"
+ >
+ {{attrTypeValue | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['opcAttributeType_' + serverIndex + mapIndex + attributeIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+
+ <section flex layout="row">
+ <md-input-container flex="100" class="md-block">
+ <label translate>extension.value</label>
+ <input required name="opcAttributeValue_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
+ ng-model="attribute.value"
+ >
+ <div ng-messages="theForm['opcAttributeValue_' + serverIndex + mapIndex + attributeIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+
+ </section>
+
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addNewAttribute(map.attributes)"
+ aria-label="{{ 'action.add' | translate }}"
+ >
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-attribute</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ <v-accordion id="opc-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
+ <v-pane id="opc-timeseries-pane_{{serverIndex}}{{mapIndex}}">
+ <v-pane-header>
+ {{ 'extension.timeseries' | translate }}
+ </v-pane-header>
+ <v-pane-content>
+ <div ng-show="map.timeseries.length > 0">
+ <ol class="list-group">
+ <li class="list-group-item"
+ ng-repeat="(timeseriesIndex, timeseries) in map.timeseries"
+ >
+ <md-button aria-label="{{ 'action.remove' | translate }}"
+ class="md-icon-button"
+ ng-click="removeItem(timeseries, map.timeseries)"
+ >
+ <ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-card>
+ <md-card-content>
+ <section flex layout="row">
+ <md-input-container flex="60" class="md-block">
+ <label translate>extension.key</label>
+ <input required
+ name="opcTimeseriesKey_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
+ ng-model="timeseries.key"
+ >
+ <div ng-messages="theForm['opcTimeseriesKey_' + serverIndex + mapIndex + timeseriesIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ <md-input-container flex="40"
+ class="md-block tb-container-for-select"
+ >
+ <label translate>extension.type</label>
+ <md-select required
+ name="opcTimeseriesType_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
+ ng-model="timeseries.type"
+ >
+ <md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
+ ng-value="attrType"
+ >
+ {{attrTypeValue | translate}}
+ </md-option>
+ </md-select>
+ <div ng-messages="theForm['opcTimeseriesType_' + serverIndex + mapIndex + timeseriesIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ <section flex layout="row">
+ <md-input-container flex="100" class="md-block">
+ <label translate>extension.value</label>
+ <input required name="opcTimeseriesValue_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
+ <div ng-messages="theForm['opcTimeseriesValue_' + serverIndex + mapIndex + timeseriesIndex].$error">
+ <div translate ng-message="required">extension.field-required</div>
+ </div>
+ </md-input-container>
+ </section>
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex layout="row" layout-align="start center">
+ <md-button class="md-primary md-raised"
+ ng-click="addNewAttribute(map.timeseries)"
+ aria-label="{{ 'action.add' | translate }}"
+ >
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-timeseries</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+ </div>
+ <div flex
+ layout="row"
+ layout-align="start center"
+ >
+ <md-button class="md-primary md-raised"
+ ng-click="addMap(server.mapping)"
+ aria-label="{{ 'action.add' | translate }}"
+ >
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.add-map</span>
+ </md-button>
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+
+ </md-card-content>
+ </md-card>
+ </li>
+ </ol>
+
+ <div flex
+ layout="row"
+ layout-align="start center"
+ >
+ <md-button class="md-primary md-raised"
+ ng-click="addServer(configuration.servers)"
+ aria-label="{{ 'action.add' | translate }}"
+ >
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>extension.opc-add-server</span>
+ </md-button>
+ </div>
+
+ </div>
+ </v-pane-content>
+ </v-pane>
+ </v-accordion>
+ <!--{{config}}-->
+ </md-card-content>
+</md-card>
\ No newline at end of file
ui/src/app/extension/extension-table.directive.js 404(+404 -0)
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
new file mode 100644
index 0000000..24e9842
--- /dev/null
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -0,0 +1,404 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import 'angular-material-data-table/dist/md-data-table.min.css';
+import './extension-table.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import extensionTableTemplate from './extension-table.tpl.html';
+import extensionDialogTemplate from './extension-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import ExtensionDialogController from './extension-dialog.controller'
+import $ from 'jquery';
+
+/*@ngInject*/
+export default function ExtensionTableDirective() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ entityId: '=',
+ entityType: '@',
+ inWidget: '@?',
+ ctx: '=?',
+ entityName: '='
+ },
+ controller: ExtensionTableController,
+ controllerAs: 'vm',
+ templateUrl: extensionTableTemplate
+ };
+}
+
+/*@ngInject*/
+function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
+
+ let vm = this;
+
+ vm.extensions = [];
+ vm.allExtensions = [];
+ vm.selectedExtensions = [];
+ vm.extensionsCount = 0;
+
+ vm.query = {
+ order: 'id',
+ limit: 5,
+ page: 1,
+ search: null
+ };
+
+ vm.enterFilterMode = enterFilterMode;
+ vm.exitFilterMode = exitFilterMode;
+ vm.onReorder = onReorder;
+ vm.onPaginate = onPaginate;
+ vm.addExtension = addExtension;
+ vm.editExtension = editExtension;
+ vm.deleteExtension = deleteExtension;
+ vm.deleteExtensions = deleteExtensions;
+ vm.reloadExtensions = reloadExtensions;
+ vm.updateExtensions = updateExtensions;
+
+ $scope.$watch("vm.entityId", function(newVal) {
+ if (newVal) {
+ if ($scope.subscriber) {
+ telemetryWebsocketService.unsubscribe($scope.subscriber);
+ $scope.subscriber = null;
+ }
+
+ vm.subscribed = false;
+ vm.syncLastTime = $translate.instant('extension.sync.not-available');
+
+ subscribeForClientAttributes();
+
+ reloadExtensions();
+ }
+ });
+
+ $scope.$on('$destroy', function() {
+ if ($scope.subscriber) {
+ telemetryWebsocketService.unsubscribe($scope.subscriber);
+ $scope.subscriber = null;
+ }
+ });
+
+ $scope.$watch("vm.query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+ updateExtensions();
+ }
+ });
+
+ $scope.$watch('vm.selectedExtensions.length', function (newLength) {
+ var selectionMode = newLength ? true : false;
+ if (vm.ctx) {
+ if (selectionMode) {
+ vm.ctx.hideTitlePanel = true;
+ $scope.$emit("selectedExtensions", true);
+ } else if (vm.query.search == null) {
+ vm.ctx.hideTitlePanel = false;
+ $scope.$emit("selectedExtensions", false);
+ }
+ }
+ });
+
+ $scope.$on("showSearch", function($event, source) {
+ if(source.entityId == vm.entityId) {
+ enterFilterMode();
+ $scope.$emit("filterMode", true);
+ }
+ });
+ $scope.$on("refreshExtensions", function($event, source) {
+ if(source.entityId == vm.entityId) {
+ reloadExtensions();
+ }
+ });
+ $scope.$on("addExtension", function($event, source) {
+ if(source.entityId == vm.entityId) {
+ addExtension();
+ }
+ });
+ $scope.$on("exportExtensions", function($event, source) {
+ if(source.entityId == vm.entityId) {
+ vm.exportExtensions(source.entityName);
+ }
+ });
+ $scope.$on("importExtensions", function($event, source) {
+ if(source.entityId == vm.entityId) {
+ vm.importExtensions();
+ }
+ });
+
+ function enterFilterMode() {
+ vm.query.search = '';
+ if(vm.inWidget) {
+ vm.ctx.hideTitlePanel = true;
+ }
+ }
+
+ function exitFilterMode() {
+ vm.query.search = null;
+ updateExtensions();
+ if(vm.inWidget) {
+ vm.ctx.hideTitlePanel = false;
+ $scope.$emit("filterMode", false);
+ }
+ }
+
+ function onReorder() {
+ updateExtensions();
+ }
+
+ function onPaginate() {
+ updateExtensions();
+ }
+
+ function addExtension($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ openExtensionDialog($event);
+ }
+
+ function editExtension($event, extension) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ openExtensionDialog($event, extension);
+ }
+
+ function openExtensionDialog($event, extension) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ var isAdd = false;
+ if(!extension) {
+ isAdd = true;
+ }
+ $mdDialog.show({
+ controller: ExtensionDialogController,
+ controllerAs: 'vm',
+ templateUrl: extensionDialogTemplate,
+ parent: angular.element($document[0].body),
+ locals: {
+ isAdd: isAdd,
+ allExtensions: vm.allExtensions,
+ entityId: vm.entityId,
+ entityType: vm.entityType,
+ extension: extension
+ },
+ bindToController: true,
+ targetEvent: $event,
+ fullscreen: true,
+ skipHide: true
+ }).then(function() {
+ reloadExtensions();
+ }, function () {
+ });
+ }
+
+ function deleteExtension($event, extension) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if(extension) {
+ var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
+ var content = $translate.instant('extension.delete-extension-text');
+
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(title)
+ .htmlContent(content)
+ .ariaLabel(title)
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function() {
+ var editedExtensions = vm.allExtensions.filter(function(ext) {
+ return ext.id !== extension.id;
+ });
+ var editedValue = angular.toJson(editedExtensions);
+ attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
+ function success() {
+ reloadExtensions();
+ }
+ );
+ });
+ }
+ }
+
+ function deleteExtensions($event) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
+ var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
+ var content = $translate.instant('extension.delete-extensions-text');
+
+ var confirm = $mdDialog.confirm()
+ .targetEvent($event)
+ .title(title)
+ .htmlContent(content)
+ .ariaLabel(title)
+ .cancel($translate.instant('action.no'))
+ .ok($translate.instant('action.yes'));
+ $mdDialog.show(confirm).then(function () {
+ var editedExtensions = angular.copy(vm.allExtensions);
+ for (var i = 0; i < vm.selectedExtensions.length; i++) {
+ editedExtensions = editedExtensions.filter(function (ext) {
+ return ext.id !== vm.selectedExtensions[i].id;
+ });
+ }
+ var editedValue = angular.toJson(editedExtensions);
+ attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
+ function success() {
+ reloadExtensions();
+ }
+ );
+ });
+ }
+ }
+
+ function reloadExtensions() {
+ vm.subscribed = false;
+ vm.allExtensions.length = 0;
+ vm.extensions.length = 0;
+ vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
+ vm.extensionsPromise.then(
+ function success(data) {
+ if (data.length) {
+ vm.allExtensions = angular.fromJson(data[0].value);
+ } else {
+ vm.allExtensions = [];
+ }
+
+ vm.selectedExtensions = [];
+ updateExtensions();
+ vm.extensionsPromise = null;
+ },
+ function fail() {
+ vm.extensions = [];
+ vm.selectedExtensions = [];
+ updateExtensions();
+ vm.extensionsPromise = null;
+ }
+ );
+ }
+
+ function updateExtensions() {
+ vm.selectedExtensions = [];
+ var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
+ if (vm.query.search != null) {
+ result = $filter('filter')(result, function(extension) {
+ if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
+ return true;
+ }
+ return false;
+ });
+ }
+ vm.extensionsCount = result.length;
+ var startIndex = vm.query.limit * (vm.query.page - 1);
+ vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
+
+ vm.extensionsJSON = angular.toJson(vm.extensions);
+ checkForSync();
+ }
+
+ function subscribeForClientAttributes() {
+ if (!vm.subscribed) {
+ if (vm.entityId && vm.entityType) {
+ $scope.subscriber = {
+ subscriptionCommands: [{
+ entityType: vm.entityType,
+ entityId: vm.entityId,
+ scope: 'CLIENT_SCOPE'
+ }],
+ type: 'attribute',
+ onData: function (data) {
+ if (data.data) {
+ onSubscriptionData(data.data);
+ }
+ vm.subscribed = true;
+ }
+ };
+ telemetryWebsocketService.subscribe($scope.subscriber);
+ }
+ }
+ }
+ function onSubscriptionData(data) {
+
+ if ($.isEmptyObject(data)) {
+ vm.appliedConfiguration = undefined;
+ } else {
+ if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) {
+ vm.appliedConfiguration = data.appliedConfiguration[0][1];
+ }
+ }
+
+ updateExtensions();
+ $scope.$digest();
+ }
+
+
+ function checkForSync() {
+ if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) {
+ vm.syncStatus = $translate.instant('extension.sync.sync');
+ vm.syncLastTime = formatDate();
+ $scope.isSync = true;
+ } else {
+ vm.syncStatus = $translate.instant('extension.sync.not-sync');
+
+ $scope.isSync = false;
+ }
+ }
+
+ function formatDate(date) {
+ let d;
+ if (date) {
+ d = date;
+ } else {
+ d = new Date();
+ }
+
+ d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds());
+ return d;
+
+ function addZero(num) {
+ if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) {
+ num = '0' + num;
+ }
+ return num;
+ }
+ }
+
+ vm.importExtensions = function($event) {
+ importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
+ };
+ vm.exportExtensions = function(widgetSourceEntityName) {
+ if(vm.inWidget) {
+ importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json');
+ } else {
+ importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json');
+ }
+ };
+
+ /*change function for widget implementing, like vm.exportExtensions*/
+ vm.exportExtension = function($event, extension) {
+ if ($event) {
+ $event.stopPropagation();
+ }
+ importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json');
+ };
+}
\ No newline at end of file
ui/src/app/extension/extension-table.scss 47(+47 -0)
diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss
new file mode 100644
index 0000000..7c0a0d8
--- /dev/null
+++ b/ui/src/app/extension/extension-table.scss
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@import '../../scss/constants';
+
+
+.extension-table {
+
+ md-input-container .md-errors-spacer {
+ min-height: 0;
+ }
+
+ /*&.tb-data-table table.md-table tbody tr td.tb-action-cell,
+ &.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
+ width: 114px;
+ }*/
+ .sync-widget {
+ max-height: 90px;
+ overflow: hidden;
+ }
+ .toolbar-widget {
+ min-height: 39px;
+ max-height: 39px;
+ }
+}
+
+.extension__syncStatus--black {
+ color: #000000!important;
+}
+.extension__syncStatus--green {
+ color: #228634!important;
+}
+.extension__syncStatus--red {
+ color: #862222!important;
+}
\ No newline at end of file
ui/src/app/extension/extension-table.tpl.html 159(+159 -0)
diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html
new file mode 100644
index 0000000..b86061c
--- /dev/null
+++ b/ui/src/app/extension/extension-table.tpl.html
@@ -0,0 +1,159 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+
+<md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
+ <div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
+ <md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
+ && vm.query.search === null">
+ <div class="md-toolbar-tools">
+ <span translate>{{ 'extension.extensions' }}</span>
+ <span flex></span>
+
+ <md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
+ <md-icon>file_upload</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'extension.import-extensions-configuration' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="vm.exportExtensions()">
+ <md-icon>file_download</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'extension.export-extensions-configuration' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="vm.addExtension($event)">
+ <md-icon>add</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.add' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
+ <md-icon>search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
+ <md-icon>refresh</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.refresh' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
+ && vm.query.search != null" ng-class="{'toolbar-widget' : vm.inWidget}">
+ <div class="md-toolbar-tools">
+ <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
+ <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.search' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-input-container flex>
+ <label> </label>
+ <input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
+ <md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length" ng-class="{'toolbar-widget' : vm.inWidget}">
+ <div class="md-toolbar-tools">
+ <span translate
+ translate-values="{count: vm.selectedExtensions.length}"
+ translate-interpolation="messageformat">extension.selected-extensions</span>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
+ <md-icon>delete</md-icon>
+ <md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
+ {{ 'action.delete' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+
+ <div class="md-padding" flex layout="row" ng-class="{'sync-widget' : vm.inWidget}">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.sync.status</label>
+ <input ng-model="vm.syncStatus"
+ ng-class="{'extension__syncStatus--green':isSync, 'extension__syncStatus--red':!isSync}"
+ disabled
+ >
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.sync.last-sync-time</label>
+ <input ng-model="vm.syncLastTime"
+ class="extension__syncStatus--black"
+ disabled
+ >
+ </md-input-container>
+ </div>
+
+ <md-table-container flex>
+ <table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
+ <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+ <tr md-row>
+ <th md-column md-order-by="id"><span translate>extension.id</span></th>
+ <th md-column md-order-by="type"><span translate>extension.type</span></th>
+ <th md-column><span> </span></th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
+ <td md-cell>{{ extension.id }}</td>
+ <td md-cell>{{ extension.type }}</td>
+ <td md-cell class="tb-action-cell">
+
+ <!--<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
+ <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'extension.export-extension' | translate }}
+ </md-tooltip>
+ </md-button>-->
+
+ <md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
+ <md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'extension.edit' | translate }}
+ </md-tooltip>
+ </md-button>
+ <md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)">
+ <md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'extension.delete' | translate }}
+ </md-tooltip>
+ </md-button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <md-divider ng-if="vm.inWidget"></md-divider>
+ </md-table-container>
+ <md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
+ md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
+ md-on-paginate="vm.onPaginate" md-page-select>
+ </md-table-pagination>
+ </div>
+
+</md-content>
\ No newline at end of file
ui/src/app/extension/index.js 29(+29 -0)
diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js
new file mode 100644
index 0000000..acb97be
--- /dev/null
+++ b/ui/src/app/extension/index.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ExtensionTableDirective from './extension-table.directive';
+import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
+import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
+import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
+import {ParseToNull} from './extension-dialog.controller';
+
+export default angular.module('thingsboard.extension', [])
+ .directive('tbExtensionTable', ExtensionTableDirective)
+ .directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
+ .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
+ .directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
+ .directive('parseToNull', ParseToNull)
+ .name;
\ No newline at end of file
ui/src/app/home/home-links.controller.js 38(+37 -1)
diff --git a/ui/src/app/home/home-links.controller.js b/ui/src/app/home/home-links.controller.js
index 273433b..b2b9366 100644
--- a/ui/src/app/home/home-links.controller.js
+++ b/ui/src/app/home/home-links.controller.js
@@ -13,8 +13,44 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import './home-links.scss';
+
/*@ngInject*/
-export default function HomeLinksController($scope, menu) {
+export default function HomeLinksController($scope, $mdMedia, menu) {
+
var vm = this;
+
+ vm.sectionColspan = sectionColspan;
+
+ $scope.$watch(function() { return $mdMedia('lg'); }, function() {
+ updateColumnCount();
+ });
+
+ $scope.$watch(function() { return $mdMedia('gt-lg'); }, function() {
+ updateColumnCount();
+ });
+
+ updateColumnCount();
+
vm.model = menu.getHomeSections();
+
+ function updateColumnCount() {
+ vm.cols = 2;
+ if ($mdMedia('lg')) {
+ vm.cols = 3;
+ }
+ if ($mdMedia('gt-lg')) {
+ vm.cols = 4;
+ }
+ }
+
+ function sectionColspan(section) {
+ var colspan = vm.cols;
+ if (section && section.places && section.places.length <= colspan) {
+ colspan = section.places.length;
+ }
+ return colspan;
+ }
+
}
ui/src/app/home/home-links.scss 26(+26 -0)
diff --git a/ui/src/app/home/home-links.scss b/ui/src/app/home/home-links.scss
new file mode 100644
index 0000000..e210334
--- /dev/null
+++ b/ui/src/app/home/home-links.scss
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import "../../scss/constants";
+
+.tb-home-links {
+ .md-headline {
+ font-size: 20px;
+ @media (min-width: $layout-breakpoint-xmd) {
+ font-size: 24px;
+ }
+ }
+}
\ No newline at end of file
ui/src/app/home/home-links.tpl.html 16(+8 -8)
diff --git a/ui/src/app/home/home-links.tpl.html b/ui/src/app/home/home-links.tpl.html
index 5eac69f..f0e5582 100644
--- a/ui/src/app/home/home-links.tpl.html
+++ b/ui/src/app/home/home-links.tpl.html
@@ -15,8 +15,8 @@
limitations under the License.
-->
-<md-grid-list md-cols="2" md-cols-gt-sm="4" md-row-height="280px">
- <md-grid-tile md-colspan="2" md-colspan-gt-sm="{{section.places.length}}" ng-repeat="section in vm.model">
+<md-grid-list class="tb-home-links" md-cols="{{vm.cols}}" md-row-height="280px">
+ <md-grid-tile md-colspan="2" md-colspan-gt-sm="{{vm.sectionColspan(section)}}" ng-repeat="section in vm.model">
<md-card style='width: 100%;'>
<md-card-title>
<md-card-title-text>
@@ -25,12 +25,12 @@
</md-card-title>
<md-card-content>
<md-grid-list md-row-height="170px" md-cols="{{section.places.length}}" md-cols-gt-md="{{section.places.length}}">
- <md-grid-tile class="card-tile" ng-repeat="place in section.places">
- <md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
- <md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
- <span translate>{{place.name}}</span>
- </md-button>
- </md-grid-tile>
+ <md-grid-tile class="card-tile" ng-repeat="place in section.places">
+ <md-button class="tb-card-button md-raised md-primary" layout="column" ui-sref="{{place.state}}">
+ <md-icon class="material-icons tb-md-96" aria-label="{{place.icon}}">{{place.icon}}</md-icon>
+ <span translate>{{place.name}}</span>
+ </md-button>
+ </md-grid-tile>
</md-grid-list>
</md-card-content>
</md-card>
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 86d5240..62e1bb3 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/* eslint-disable no-undef, angular/window-service, angular/document-service */
/*@ngInject*/
-export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils,
- entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
+export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
+ dashboardUtils, entityService, dashboardService, pluginService, ruleService,
+ widgetService, toast, attributeService) {
var service = {
@@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
exportWidgetType: exportWidgetType,
importWidgetType: importWidgetType,
exportWidgetsBundle: exportWidgetsBundle,
- importWidgetsBundle: importWidgetsBundle
- }
+ importWidgetsBundle: importWidgetsBundle,
+ exportExtension: exportExtension,
+ importExtension: importExtension,
+ exportToPc: exportToPc
+ };
return service;
@@ -614,6 +618,84 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
+
+
+ function exportExtension(extensionId) {
+
+ getExtension(extensionId)
+ .then(
+ function success(extension) {
+ var name = extension.title;
+ name = name.toLowerCase().replace(/\W/g,"_");
+ exportToPc(prepareExport(extension), name + '.json');
+ },
+ function fail(rejection) {
+ var message = rejection;
+ if (!message) {
+ message = $translate.instant('error.unknown-error');
+ }
+ toast.showError($translate.instant('extension.export-failed-error', {error: message}));
+ }
+ );
+
+ function getExtension(extensionId) {
+ var deferred = $q.defer();
+ var url = '/api/plugins/telemetry/DEVICE/' + extensionId;
+ $http.get(url, null)
+ .then(function success(response) {
+ deferred.resolve(response.data);
+ }, function fail() {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ }
+
+ function importExtension($event, options) {
+ var deferred = $q.defer();
+ openImportDialog($event, 'extension.import-extensions', 'extension.file')
+ .then(
+ function success(extension) {
+ if (!validateImportedExtension(extension)) {
+ toast.showError($translate.instant('extension.invalid-file-error'));
+ deferred.reject();
+ } else {
+ attributeService
+ .saveEntityAttributes(
+ options.entityType,
+ options.entityId,
+ types.attributesScope.shared.value,
+ [{
+ key: "configuration",
+ value: angular.toJson(extension)
+ }]
+ )
+ .then(function success() {
+ options.successFunc();
+ });
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function validateImportedExtension(configuration) {
+ if (configuration.length) {
+ for (let i = 0; i < configuration.length; i++) {
+ if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ return true;
+ }
+
function processEntityAliases(entityAliases, aliasIds) {
var deferred = $q.defer();
var missingEntityAliases = {};
ui/src/app/layout/index.js 2(+2 -0)
diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js
index 9d3e1d1..c23b008 100644
--- a/ui/src/app/layout/index.js
+++ b/ui/src/app/layout/index.js
@@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
import thingsboardEntity from '../entity';
import thingsboardEvent from '../event';
import thingsboardAlarm from '../alarm';
+import thingsboardExtension from '../extension';
import thingsboardTenant from '../tenant';
import thingsboardCustomer from '../customer';
import thingsboardUser from '../user';
@@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [
thingsboardEntity,
thingsboardEvent,
thingsboardAlarm,
+ thingsboardExtension,
thingsboardTenant,
thingsboardCustomer,
thingsboardUser,
ui/src/app/locale/locale.constant.js 131(+129 -2)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index bd1b9e9..ab672de 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -729,6 +729,133 @@ export default angular.module('thingsboard.locale', [])
"messages-processed": "Messages processed",
"errors-occurred": "Errors occurred"
},
+ "extension": {
+ "extensions": "Extensions",
+ "selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
+ "type": "Type",
+ "key": "Key",
+ "value": "Value",
+ "id": "Id",
+ "extension-id": "Extension id",
+ "extension-type": "Extension type",
+ "transformer-json": "JSON *",
+ "unique-id-required": "Current extension id already exists.",
+ "delete": "Delete extension",
+ "add": "Add extension",
+ "edit": "Edit extension",
+ "delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
+ "delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
+ "delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
+ "delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
+ "converters": "Converters",
+ "converter-id": "Converter id",
+ "configuration": "Configuration",
+ "converter-configurations": "Converter configurations",
+ "token": "Security token",
+ "add-converter": "Add converter",
+ "add-config": "Add converter configuration",
+ "device-name-expression": "Device name expression",
+ "device-type-expression": "Device type expression",
+ "custom": "Custom",
+ "to-double": "To Double",
+ "transformer": "Transformer",
+ "json-required": "Transformer json is required.",
+ "json-parse": "Unable to parse transformer json.",
+ "attributes": "Attributes",
+ "add-attribute": "Add attribute",
+ "add-map": "Add mapping element",
+ "timeseries": "Timeseries",
+ "add-timeseries": "Add timeseries",
+ "field-required": "Field is required",
+ "brokers": "Brokers",
+ "add-broker": "Add broker",
+ "host": "Host",
+ "port": "Port",
+ "port-range": "Port should be in a range from 1 to 65535.",
+ "ssl": "Ssl",
+ "credentials": "Credentials",
+ "username": "Username",
+ "password": "Password",
+ "retry-interval": "Retry interval in milliseconds",
+ "anonymous": "Anonymous",
+ "basic": "Basic",
+ "pem": "PEM",
+ "ca-cert": "CA certificate file *",
+ "private-key": "Private key file *",
+ "cert": "Certificate file *",
+ "no-file": "No file selected.",
+ "drop-file": "Drop a file or click to select a file to upload.",
+ "mapping": "Mapping",
+ "topic-filter": "Topic filter",
+ "converter-type": "Converter type",
+ "converter-json": "Json",
+ "json-name-expression": "Device name json expression",
+ "topic-name-expression": "Device name topic expression",
+ "json-type-expression": "Device type json expression",
+ "topic-type-expression": "Device type topic expression",
+ "attribute-key-expression": "Attribute key expression",
+ "attr-json-key-expression": "Attribute key json expression",
+ "attr-topic-key-expression": "Attribute key topic expression",
+ "request-id-expression": "Request id expression",
+ "request-id-json-expression": "Request id json expression",
+ "request-id-topic-expression": "Request id topic expression",
+ "response-topic-expression": "Response topic expression",
+ "value-expression": "Value expression",
+ "topic": "Topic",
+ "timeout": "Timeout in milliseconds",
+ "converter-json-required": "Converter json is required.",
+ "converter-json-parse": "Unable to parse converter json.",
+ "filter-expression": "Filter expression",
+ "connect-requests": "Connect requests",
+ "add-connect-request": "Add connect request",
+ "disconnect-requests": "Disconnect requests",
+ "add-disconnect-request": "Add disconnect request",
+ "attribute-requests": "Attribute requests",
+ "add-attribute-request": "Add attribute request",
+ "attribute-updates": "Attribute updates",
+ "add-attribute-update": "Add attribute update",
+ "server-side-rpc": "Server side RPC",
+ "add-server-side-rpc-request": "Add server-side RPC request",
+ "device-name-filter": "Device name filter",
+ "attribute-filter": "Attribute filter",
+ "method-filter": "Method filter",
+ "request-topic-expression": "Request topic expression",
+ "response-timeout": "Response timeout in milliseconds",
+ "topic-expression": "Topic expression",
+ "client-scope": "Client scope",
+ "opc-server": "Servers",
+ "opc-add-server": "Add server",
+ "opc-application-name": "Application name",
+ "opc-application-uri": "Application uri",
+ "opc-scan-period-in-seconds": "Scan period in seconds",
+ "opc-security": "Security",
+ "opc-identity": "Identity",
+ "opc-keystore": "Keystore",
+ "opc-type": "Type",
+ "opc-keystore-type":"Type",
+ "opc-keystore-location":"Location *",
+ "opc-keystore-password":"Password",
+ "opc-keystore-alias":"Alias",
+ "opc-keystore-key-password":"Key password",
+ "opc-device-node-pattern":"Device node pattern",
+ "opc-device-name-pattern":"Device name pattern",
+
+ "sync": {
+ "status": "Status",
+ "sync": "Sync",
+ "not-sync": "Not sync",
+ "last-sync-time": "Last sync time",
+ "not-available": "Not available"
+ },
+
+ "export-extensions-configuration":"Export extensions configuration",
+ "import-extensions-configuration":"Import extensions configuration",
+ "import-extensions": "Import extensions",
+ "import-extension": "Import extension",
+ "export-extension": "Export extension",
+ "file": "Extensions file",
+ "invalid-file-error": "Invalid extension file"
+ },
"fullscreen": {
"expand": "Expand to fullscreen",
"exit": "Exit fullscreen",
@@ -851,7 +978,6 @@ export default angular.module('thingsboard.locale', [])
"invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
"copyId": "Copy plugin Id",
"idCopiedMessage": "Plugin Id has been copied to clipboard"
-
},
"position": {
"top": "Top",
@@ -1071,7 +1197,8 @@ export default angular.module('thingsboard.locale', [])
"boolean": "Boolean",
"boolean-value": "Boolean value",
"false": "False",
- "true": "True"
+ "true": "True",
+ "long": "Long"
},
"widget": {
"widget-library": "Widgets Library",
diff --git a/ui/src/app/widget/lib/analogue-compass.js b/ui/src/app/widget/lib/analogue-compass.js
index 46059c7..85ec0a6 100644
--- a/ui/src/app/widget/lib/analogue-compass.js
+++ b/ui/src/app/widget/lib/analogue-compass.js
@@ -104,7 +104,9 @@ export default class TbAnalogueCompass {
var tvPair = cellData.data[cellData.data.length -
1];
var value = tvPair[1];
- this.gauge.value = value;
+ if(value !== this.gauge.value) {
+ this.gauge.value = value;
+ }
}
}
}
diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js
index a045f94..da4e386 100644
--- a/ui/src/app/widget/lib/analogue-linear-gauge.js
+++ b/ui/src/app/widget/lib/analogue-linear-gauge.js
@@ -212,7 +212,9 @@ export default class TbAnalogueLinearGauge {
var tvPair = cellData.data[cellData.data.length -
1];
var value = tvPair[1];
- this.gauge.value = value;
+ if(value !== this.gauge.value) {
+ this.gauge.value = value;
+ }
}
}
}
diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js
index 76a2c38..7a5c0c7 100644
--- a/ui/src/app/widget/lib/analogue-radial-gauge.js
+++ b/ui/src/app/widget/lib/analogue-radial-gauge.js
@@ -221,7 +221,9 @@ export default class TbAnalogueRadialGauge {
var tvPair = cellData.data[cellData.data.length -
1];
var value = tvPair[1];
- this.gauge.value = value;
+ if(value !== this.gauge.value) {
+ this.gauge.value = value;
+ }
}
}
diff --git a/ui/src/app/widget/lib/canvas-digital-gauge.js b/ui/src/app/widget/lib/canvas-digital-gauge.js
index 74ab7ac..231fb6f 100644
--- a/ui/src/app/widget/lib/canvas-digital-gauge.js
+++ b/ui/src/app/widget/lib/canvas-digital-gauge.js
@@ -197,14 +197,19 @@ export default class TbCanvasDigitalGauge {
if (cellData.data.length > 0) {
var tvPair = cellData.data[cellData.data.length -
1];
+ var timestamp;
if (this.localSettings.showTimestamp) {
- var timestamp = tvPair[0];
+ timestamp = tvPair[0];
var filter= this.ctx.$scope.$injector.get('$filter');
var timestampDisplayValue = filter('date')(timestamp, this.localSettings.timestampFormat);
this.gauge.options.label = timestampDisplayValue;
}
var value = tvPair[1];
- this.gauge.value = value;
+ if(value !== this.gauge.value) {
+ this.gauge.value = value;
+ } else if (this.localSettings.showTimestamp && this.gauge.timestamp != timestamp) {
+ this.gauge.timestamp = timestamp;
+ }
}
}
}
ui/src/app/widget/lib/CanvasDigitalGauge.js 18(+16 -2)
diff --git a/ui/src/app/widget/lib/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js
index a1cfca2..bd3ff98 100644
--- a/ui/src/app/widget/lib/CanvasDigitalGauge.js
+++ b/ui/src/app/widget/lib/CanvasDigitalGauge.js
@@ -155,6 +155,15 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
return result;
}
+ set timestamp(timestamp) {
+ this.options.timestamp = timestamp;
+ this.draw();
+ }
+
+ get timestamp() {
+ return this.options.timestamp;
+ }
+
draw() {
try {
@@ -195,7 +204,9 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
canvas.elementClone.initialized = true;
}
- if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value) {
+ var valueChanged = false;
+
+ if (!this.elementValueClone.initialized || this.elementValueClone.renderedValue !== this.value || (options.showTimestamp && this.elementValueClone.renderedTimestamp !== this.timestamp)) {
let context = this.contextValueClone;
// clear the cache
context.clearRect(x, y, w, h);
@@ -208,10 +219,13 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
if (options.showTimestamp) {
drawDigitalLabel(context, options);
+ this.elementValueClone.renderedTimestamp = this.timestamp;
}
this.elementValueClone.initialized = true;
this.elementValueClone.renderedValue = this.value;
+
+ valueChanged = true;
}
var progress = (canvasGauges.drawings.normalizedValue(options).normal - options.minValue) /
@@ -219,7 +233,7 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
var fixedProgress = progress.toFixed(3);
- if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress) {
+ if (!this.elementProgressClone.initialized || this.elementProgressClone.renderedProgress !== fixedProgress || valueChanged) {
let context = this.contextProgressClone;
// clear the cache
context.clearRect(x, y, w, h);
ui/src/app/widget/lib/extensions-table-widget.js 135(+135 -0)
diff --git a/ui/src/app/widget/lib/extensions-table-widget.js b/ui/src/app/widget/lib/extensions-table-widget.js
new file mode 100644
index 0000000..3e5ac61
--- /dev/null
+++ b/ui/src/app/widget/lib/extensions-table-widget.js
@@ -0,0 +1,135 @@
+/*
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './extensions-table-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+export default angular.module('thingsboard.widgets.extensionsTableWidget', [])
+ .directive('tbExtensionsTableWidget', ExtensionsTableWidget)
+ .name;
+
+/*@ngInject*/
+function ExtensionsTableWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ ctx: '='
+ },
+ controller: ExtensionsTableWidgetController,
+ controllerAs: 'vm',
+ templateUrl: extensionsTableWidgetTemplate
+ };
+}
+
+/*@ngInject*/
+function ExtensionsTableWidgetController($scope, $translate, utils) {
+ var vm = this;
+
+ vm.datasources = null;
+ vm.tabsHidden = false;
+
+ $scope.$watch('vm.ctx', function() {
+ if (vm.ctx && vm.ctx.defaultSubscription) {
+ vm.settings = vm.ctx.settings;
+ vm.subscription = vm.ctx.defaultSubscription;
+ vm.datasources = vm.subscription.datasources;
+ initializeConfig();
+ updateDatasources();
+ }
+ });
+
+ function initializeConfig() {
+
+ if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) {
+ vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle);
+ } else {
+ vm.extensionsTitle = $translate.instant('extension.extensions');
+ }
+ vm.ctx.widgetTitle = vm.extensionsTitle;
+
+ vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction];
+ }
+
+ function updateDatasources() {
+
+ var datasource = vm.datasources[0];
+ vm.selectedSource = vm.datasources[0];
+ vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle);
+ }
+
+ vm.changeSelectedSource = function(source) {
+ vm.selectedSource = source;
+ };
+
+ vm.searchAction = {
+ name: "action.search",
+ show: true,
+ onAction: function() {
+ $scope.$broadcast("showSearch", vm.selectedSource);
+ },
+ icon: "search"
+ };
+
+ vm.refreshAction = {
+ name: "action.refresh",
+ show: true,
+ onAction: function() {
+ $scope.$broadcast("refreshExtensions", vm.selectedSource);
+ },
+ icon: "refresh"
+ };
+
+ vm.addAction = {
+ name: "action.add",
+ show: true,
+ onAction: function() {
+ $scope.$broadcast("addExtension", vm.selectedSource);
+ },
+ icon: "add"
+ };
+
+ vm.exportExtensionsAction = {
+ name: "extension.export-extensions-configuration",
+ show: true,
+ onAction: function() {
+ $scope.$broadcast("exportExtensions", vm.selectedSource);
+ },
+ icon: "file_download"
+ };
+
+ vm.importExtensionsAction = {
+ name: "extension.import-extensions-configuration",
+ show: true,
+ onAction: function() {
+ $scope.$broadcast("importExtensions", vm.selectedSource);
+ },
+ icon: "file_upload"
+ };
+
+ $scope.$on("filterMode", function($event, mode) {
+ vm.tabsHidden = mode;
+ });
+
+ $scope.$on("selectedExtensions", function($event, mode) {
+ vm.tabsHidden = mode;
+ });
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/extensions-table-widget.scss b/ui/src/app/widget/lib/extensions-table-widget.scss
new file mode 100644
index 0000000..26ab5d9
--- /dev/null
+++ b/ui/src/app/widget/lib/extensions-table-widget.scss
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+tb-extension-table {
+ md-content {
+ background-color: #fff;
+ }
+}
+md-tabs.hide-tabs-menu {
+ md-tabs-wrapper {
+ display: none;
+ }
+ md-tabs-content-wrapper {
+ top: 0 !important;
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/extensions-table-widget.tpl.html b/ui/src/app/widget/lib/extensions-table-widget.tpl.html
new file mode 100644
index 0000000..0671938
--- /dev/null
+++ b/ui/src/app/widget/lib/extensions-table-widget.tpl.html
@@ -0,0 +1,27 @@
+<!--
+
+ Copyright © 2016-2017 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-tabs id="tabs" md-border-bottom flex class="tb-absolute-fill" ng-class="{'hide-tabs-menu': vm.datasources.length == 1 || vm.tabsHidden}">
+ <md-tab ng-repeat="source in vm.datasources" label="{{ source.name }}" md-on-select="vm.changeSelectedSource(source)">
+ <tb-extension-table flex
+ entity-id="source.entityId"
+ entity-type="{{source.entityType}}"
+ in-widget="true"
+ ctx="vm.ctx">
+ </tb-extension-table>
+ </md-tab>
+</md-tabs>
\ No newline at end of file
ui/src/index.html 2(+1 -1)
diff --git a/ui/src/index.html b/ui/src/index.html
index 2f4045a..33702cc 100644
--- a/ui/src/index.html
+++ b/ui/src/index.html
@@ -16,7 +16,7 @@
-->
<!DOCTYPE html>
-<html ng-app="thingsboard" ng-strict-di>
+<html ng-app="thingsboard" ng-strict-di style="width: 100%;">
<head>
<title ng-bind="pageTitle"></title>
<base href="/" />
ui/src/scss/animations.scss 2(+1 -1)
diff --git a/ui/src/scss/animations.scss b/ui/src/scss/animations.scss
index e7d9820..ab27252 100644
--- a/ui/src/scss/animations.scss
+++ b/ui/src/scss/animations.scss
@@ -39,6 +39,6 @@
@include keyframes(tbMoveToBottomFade) {
to {
opacity: 0;
- @include transform(translate(0, 100%));
+ @include transform(translate(0, 150%));
}
}
\ No newline at end of file
ui/src/scss/main.scss 6(+5 -1)
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 076dbef..93418b2 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -494,11 +494,15 @@ md-tabs.tb-headless {
height: 100%;
max-width: 240px;
span {
- padding: 10px 10px 20px 10px;
+ padding: 0 0 20px 0;
font-size: 18px;
font-weight: 400;
white-space: normal;
line-height: 18px;
+ max-height: 18px;
+ min-height: 18px;
+ height: 18px;
+ margin: auto;
}
}