diff --git a/application/pom.xml b/application/pom.xml index 890da91e1e..c7d3240ef4 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -496,6 +496,16 @@ extension-mqtt extension + + org.thingsboard.extensions + extension-sqs + extension + + + org.thingsboard.extensions + extension-sns + extension + 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 164366dd84..d811393c4b 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 034209e8c1..0fc0085f12 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 0000000000..c963835ef9 --- /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": "\n", + "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 c695ec8316..7a7e3ff1ab 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 { Optional> 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 639579863a..6755408b0c 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; @@ -147,6 +148,11 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt return new BCryptPasswordEncoder(); } + @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() 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 0246e07bb6..cb2f5b9a33 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 689f3164f6..19e4329c33 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 8dac038772..e3c87f66c2 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") diff --git a/common/data/pom.xml b/common/data/pom.xml index 2c9603b724..11e62e8c8e 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server Common Data - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/common/message/pom.xml b/common/message/pom.xml index 3f36cdf88f..33466a6101 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server Common Messages - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/common/pom.xml b/common/pom.xml index 0320a4a948..dfba5288be 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -28,7 +28,7 @@ pom Thingsboard Server Commons - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/common/transport/pom.xml b/common/transport/pom.xml index f609e9a2c8..884f455e3b 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server Common Transport components - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/dao/pom.xml b/dao/pom.xml index 9805b6bb8a..d9463e4f9c 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server DAO Layer - http://thingsboard.org + https://thingsboard.io UTF-8 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 3574e83d7d..86afdb488d 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 children = new HashSet<>(findRelations(rootId, direction).get()); Set 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 7c34aa1d86..78282674ab 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 7fddfaeebf..170f9a6c2c 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 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(); + } + } diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml index 1305c98c07..298da9a6cc 100644 --- a/extensions-api/pom.xml +++ b/extensions-api/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server Extensions API - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml index f94264ea79..14f91a1eaf 100644 --- a/extensions-core/pom.xml +++ b/extensions-core/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server Core Extensions - http://thingsboard.org + https://thingsboard.io UTF-8 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 0b8e992525..7e92fc15f1 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 interval = request.getLongParamValue("interval"); Optional 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 queries = keys.stream().map(key -> new BaseTsKvQuery(key, startTs.get(), endTs.get(), interval.get(), limit.orElse(TelemetryWebsocketMsgHandler.DEFAULT_LIMIT), agg)) .collect(Collectors.toList()); diff --git a/extensions/extension-kafka/pom.xml b/extensions/extension-kafka/pom.xml index 6342be4b2e..c53fc72bad 100644 --- a/extensions/extension-kafka/pom.xml +++ b/extensions/extension-kafka/pom.xml @@ -30,7 +30,7 @@ jar Thingsboard Server Kafka Extension - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/extensions/extension-mqtt/pom.xml b/extensions/extension-mqtt/pom.xml index 85f511dc9f..517fbd3444 100644 --- a/extensions/extension-mqtt/pom.xml +++ b/extensions/extension-mqtt/pom.xml @@ -30,7 +30,7 @@ jar Thingsboard Server MQTT Extension - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/extensions/extension-rabbitmq/pom.xml b/extensions/extension-rabbitmq/pom.xml index ba5e6d2e6a..9f9567060d 100644 --- a/extensions/extension-rabbitmq/pom.xml +++ b/extensions/extension-rabbitmq/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server RabbitMQ Extension - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/extensions/extension-rest-api-call/pom.xml b/extensions/extension-rest-api-call/pom.xml index af5d19651e..a7872b8dee 100644 --- a/extensions/extension-rest-api-call/pom.xml +++ b/extensions/extension-rest-api-call/pom.xml @@ -30,7 +30,7 @@ jar Thingsboard Server REST API Call Extension - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/extensions/extension-sns/pom.xml b/extensions/extension-sns/pom.xml new file mode 100644 index 0000000000..ae3177301d --- /dev/null +++ b/extensions/extension-sns/pom.xml @@ -0,0 +1,81 @@ + + + + + extensions + org.thingsboard + 1.4.0-SNAPSHOT + + 4.0.0 + org.thingsboard.extensions + extension-sns + jar + + Thingsboard Server SNS Extension + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + 1.11.229 + + + + + org.thingsboard + extensions-api + provided + + + org.thingsboard + extensions-core + provided + + + com.amazonaws + aws-java-sdk-sns + ${aws.sdk.version} + + + + + + + maven-assembly-plugin + + + src/assembly/extension.xml + + + + + make-assembly + package + + single + + + + + + + + \ 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 0000000000..2395d5551a --- /dev/null +++ b/extensions/extension-sns/src/assembly/extension.xml @@ -0,0 +1,37 @@ + + + extension + + jar + + false + + + / + true + true + runtime + + + + + + \ 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 0000000000..b64a22bdba --- /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 { + + 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 0000000000..a1bf89a248 --- /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 0000000000..79de23acd4 --- /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 { + + @Override + protected Optional 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 0000000000..468c3ddbc3 --- /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 0000000000..c7fd99c8be --- /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 0000000000..86f5de0331 --- /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 { + + 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 0000000000..dee78f3f57 --- /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 0000000000..04e23e75bc --- /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 0000000000..a8a2793885 --- /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 diff --git a/extensions/extension-sqs/pom.xml b/extensions/extension-sqs/pom.xml new file mode 100644 index 0000000000..7f3818d3d9 --- /dev/null +++ b/extensions/extension-sqs/pom.xml @@ -0,0 +1,81 @@ + + + + + extensions + org.thingsboard + 1.4.0-SNAPSHOT + + 4.0.0 + org.thingsboard.extensions + extension-sqs + jar + + Thingsboard Server SQS Extension + https://thingsboard.io + + + UTF-8 + ${basedir}/../.. + 1.11.229 + + + + + org.thingsboard + extensions-api + provided + + + org.thingsboard + extensions-core + provided + + + com.amazonaws + aws-java-sdk-sqs + ${aws.sdk.version} + + + + + + + maven-assembly-plugin + + + src/assembly/extension.xml + + + + + make-assembly + package + + single + + + + + + + + \ 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 0000000000..2395d5551a --- /dev/null +++ b/extensions/extension-sqs/src/assembly/extension.xml @@ -0,0 +1,37 @@ + + + extension + + jar + + false + + + / + true + true + runtime + + + + + + \ 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 0000000000..c4650636f3 --- /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 { + + 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 0000000000..692cd90849 --- /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 0000000000..f107ea103c --- /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 { + + @Override + protected Optional 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 0000000000..d10032576b --- /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 0000000000..6666b274f3 --- /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 { + + 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 0000000000..ce3dd27a8d --- /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 0000000000..1dd9d56d8a --- /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 { + + @Override + protected Optional 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 0000000000..5c21ef5840 --- /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 0000000000..b71e0b18b7 --- /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 0000000000..3f0252ebf6 --- /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 { + + 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 0000000000..a93b7f3f1d --- /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 0000000000..05bfaec11f --- /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 0000000000..407468b5c8 --- /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 0000000000..f5502e6a25 --- /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 0000000000..17c9b08e8c --- /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 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 0000000000..757aa32dea --- /dev/null +++ b/extensions/extension-sqs/src/test/resources/logback.xml @@ -0,0 +1,10 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + \ No newline at end of file diff --git a/extensions/pom.xml b/extensions/pom.xml index 30c939556e..50dca83baf 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -28,7 +28,7 @@ pom Thingsboard Extensions - http://thingsboard.org + https://thingsboard.io ${basedir}/.. @@ -39,6 +39,8 @@ extension-rest-api-call extension-kafka extension-mqtt + extension-sqs + extension-sns diff --git a/pom.xml b/pom.xml index 1f9a92d97b..e45fc3ea28 100755 --- a/pom.xml +++ b/pom.xml @@ -350,6 +350,18 @@ extension ${project.version} + + org.thingsboard.extensions + extension-sqs + extension + ${project.version} + + + org.thingsboard.extensions + extension-sns + extension + ${project.version} + org.thingsboard.common data diff --git a/resume.bat b/resume.bat new file mode 100644 index 0000000000..c3c0e1d817 --- /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 + diff --git a/tools/pom.xml b/tools/pom.xml index f0ade6eac2..5b74b56200 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server Tools - http://thingsboard.org + https://thingsboard.io UTF-8 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 d635dbcb3e..7000496cdc 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 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 0000000000..8c4263d4ce --- /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) diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index c67b2a8cd0..721202339a 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard COAP Transport - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 58d5d64690..dc5dfa5b1e 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard HTTP Transport - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 414ca9f117..d8f57030a2 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard MQTT Transport - http://thingsboard.org + https://thingsboard.io UTF-8 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 2e6abfde96..7e4c2eac9c 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 7b527dff83..7bed03ac48 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 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 booleanValue = entry.getBooleanValue(); - if (booleanValue.isPresent()) { - json.addProperty(name, booleanValue.get()); - } + entry.getBooleanValue().ifPresent(aBoolean -> json.addProperty(name, aBoolean)); break; case STRING: - Optional stringValue = entry.getStrValue(); - if (stringValue.isPresent()) { - json.addProperty(name, stringValue.get()); - } + entry.getStrValue().ifPresent(aString -> json.addProperty(name, aString)); break; case DOUBLE: - Optional doubleValue = entry.getDoubleValue(); - if (doubleValue.isPresent()) { - json.addProperty(name, doubleValue.get()); - } + entry.getDoubleValue().ifPresent(aDouble -> json.addProperty(name, aDouble)); break; case LONG: - Optional 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 00d1f0c6d7..d69341c999 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 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())); } } diff --git a/transport/pom.xml b/transport/pom.xml index f9b67e350e..63b1fe127c 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -28,7 +28,7 @@ pom Thingsboard Server Transport Modules - http://thingsboard.org + https://thingsboard.io ${basedir}/.. diff --git a/ui/package.json b/ui/package.json index cb8fdbd913..81f457417a 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", diff --git a/ui/pom.xml b/ui/pom.xml index d84eaf5db5..e840067c84 100644 --- a/ui/pom.xml +++ b/ui/pom.xml @@ -28,7 +28,7 @@ jar Thingsboard Server UI - http://thingsboard.org + https://thingsboard.io UTF-8 diff --git a/ui/src/app/api/login.service.js b/ui/src/app/api/login.service.js index 272e4dfa20..74f9587cf0 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(); diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 16686576d3..d09387bda8 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; } diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 0df17b74c7..3c74ecbb4c 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; diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 4a6f3f0a8d..5b1908928e 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', diff --git a/ui/src/app/asset/asset-card.tpl.html b/ui/src/app/asset/asset-card.tpl.html index 30d0483934..b02b2e23ac 100644 --- a/ui/src/app/asset/asset-card.tpl.html +++ b/ui/src/app/asset/asset-card.tpl.html @@ -16,7 +16,7 @@ --> - {{vm.item.type}} + {{vm.item.type}} {{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}' {{'asset.public' | translate}} diff --git a/ui/src/app/asset/assign-to-customer.controller.js b/ui/src/app/asset/assign-to-customer.controller.js index 602e599ae9..3df90ef9d1 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 gridSettings.columns) { + widget.sizeX = gridSettings.columns - widget.col; + } } } diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 7bac29dff9..5b3c9e4ac4 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 f27a9a549b..28b07347ed 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')) diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js index 6605692d27..c489c32f7e 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 {}; + } + } + } + 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 54a029dc37..159162a50e 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; diff --git a/ui/src/app/components/js-func.scss b/ui/src/app/components/js-func.scss index 7c36b8101e..3124dc12d3 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 ee3c2e0947..bec7991af9 100644 --- a/ui/src/app/components/js-func.tpl.html +++ b/ui/src/app/components/js-func.tpl.html @@ -19,7 +19,7 @@ function({{ functionArgsString }}) { - + - {{vm.item.type}} + {{vm.item.type}} {{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}' {{'device.public' | translate}} diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index b8394c7958..1ec0134444 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}}"> + + + + diff --git a/ui/src/app/entity/attribute/attribute-table.tpl.html b/ui/src/app/entity/attribute/attribute-table.tpl.html index 6f862ba71f..3b19e9d8d7 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 @@ --> - + attribute.attributes-scope @@ -26,7 +26,7 @@ - + diff --git a/ui/src/app/entity/relation/relation-dialog.controller.js b/ui/src/app/entity/relation/relation-dialog.controller.js index ae50e3033e..d13b4dbba2 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 55064df8fa..1bd038ba01 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 15b0c7f47b..8799287637 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. --> - + 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 0000000000..fe5825c8ee --- /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 0) { + for(let i=0;i 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j 0) { + for(let j=0;j + + + + + {{ vm.isAdd ? 'extension.add' : 'extension.edit'}} + + + + + + + + + + + + + + + + + + extension.extension-id + + + extension.field-required + extension.unique-id-required + + + + + extension.extension-type + + + + {{value}} + + + + + extension.field-required + + + + + + + + + + + + + + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} + + + {{ 'action.cancel' | translate }} + + + + \ No newline at end of file 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 0000000000..24e98420f1 --- /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 diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss new file mode 100644 index 0000000000..7c0a0d8578 --- /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 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 0000000000..b86061cdce --- /dev/null +++ b/ui/src/app/extension/extension-table.tpl.html @@ -0,0 +1,159 @@ + + + + + + + {{ 'extension.extensions' }} + + + + file_upload + + {{ 'extension.import-extensions-configuration' | translate }} + + + + file_download + + {{ 'extension.export-extensions-configuration' | translate }} + + + + add + + {{ 'action.add' | translate }} + + + + search + + {{ 'action.search' | translate }} + + + + refresh + + {{ 'action.refresh' | translate }} + + + + + + + + search + + {{ 'action.search' | translate }} + + + + + + + + close + + {{ 'action.close' | translate }} + + + + + + + extension.selected-extensions + + + delete + + {{ 'action.delete' | translate }} + + + + + + + + extension.sync.status + + + + + extension.sync.last-sync-time + + + + + + + + + extension.id + extension.type +   + + + + + {{ extension.id }} + {{ extension.type }} + + + + + + edit + + {{ 'extension.edit' | translate }} + + + + delete + + {{ 'extension.delete' | translate }} + + + + + + + + + + + + + \ 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 0000000000..37a94443f7 --- /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 0000000000..b39794a725 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html @@ -0,0 +1,311 @@ + + + + + extension.configuration + + + + + + + {{ 'extension.converter-configurations' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.converter-id + + + extension.field-required + + + + extension.token + + + + + + {{ 'extension.converters' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + extension.device-name-expression + + + extension.field-required + + + + extension.device-type-expression + + + extension.field-required + + + + + + + {{ 'extension.attributes' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.key + + + extension.field-required + + + + extension.type + + + {{attrTypeValue | translate}} + + + + extension.field-required + + + + + + extension.value + + + extension.field-required + + + + + + extension.transformer + + + {{value | translate}} + + + + + + + extension.transformer-json + + + + + + extension.json-required + extension.json-parse + + + + + + + + + + + + add + extension.add-attribute + + + + + + + + + + + {{ 'extension.timeseries' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.key + + + extension.field-required + + + + extension.type + + + {{attrTypeValue | translate}} + + + + extension.field-required + + + + + + extension.value + + + extension.field-required + + + + + + extension.transformer + + + {{value | translate}} + + + + + + + extension.transformer-json + + + + + + extension.json-required + extension.json-parse + + + + + + + + + + + + add + extension.add-timeseries + + + + + + + + + + + + + add + extension.add-converter + + + + + + + + + + + + + + add + extension.add-config + + + + + + + + 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 0000000000..39a1eb8fcc --- /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 -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 0000000000..f665fb0ef0 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html @@ -0,0 +1,860 @@ + + + + + extension.configuration + + + + + + + {{ 'extension.brokers' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.port + + + extension.field-required + extension.port-range + extension.port-range + + + + extension.host + + + extension.field-required + + + + + + extension.retry-interval + + + extension.field-required + + + + extension.credentials + + + {{credentialsValue.name | translate}} + + + + + {{ 'extension.ssl' | translate }} + + + + + + extension.username + + + extension.field-required + + + + extension.password + + + extension.field-required + + + + + + extension.ca-cert + + + + + {{ 'action.remove' | translate }} + + close + + + + extension.drop-file + + + + + + extension.no-file + {{broker.credentials.caCertFileName}} + + + extension.private-key + + + + + {{ 'action.remove' | translate }} + + close + + + + extension.drop-file + + + + + + extension.no-file + {{broker.credentials.privateKeyFileName}} + + + extension.cert + + + + + {{ 'action.remove' | translate }} + + close + + + + extension.drop-file + + + + + + extension.no-file + {{broker.credentials.certFileName}} + + + + + + + {{ 'extension.mapping' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.converter-type + + + {{value | translate}} + + + + extension.field-required + + + + extension.topic-filter + + + extension.field-required + + + + + + + + extension.device-name-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.json-name-expression + + + extension.field-required + + + + extension.topic-name-expression + + + extension.field-required + + + + + + extension.device-type-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.json-type-expression + + + extension.field-required + + + + extension.topic-type-expression + + + extension.field-required + + + + + + extension.timeout + + + + extension.filter-expression + + + extension.field-required + + + + + + + extension.transformer-json + + + + + + extension.converter-json-required + extension.converter-json-parse + + + + + + + {{ 'extension.attributes' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.key + + + extension.field-required + + + + extension.type + + + {{attrTypeValue | translate}} + + + + extension.field-required + + + + + extension.value + + + extension.field-required + + + + + + + + + + add + extension.add-attribute + + + + + + + + + + {{ 'extension.timeseries' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.key + + + extension.field-required + + + + extension.type + + + {{attrTypeValue | translate}} + + + + extension.field-required + + + + + extension.value + + + extension.field-required + + + + + + + + + + add + extension.add-timeseries + + + + + + + + + + + + + + add + extension.add-map + + + + + + + + + + {{ 'extension.connect-requests' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + extension.topic-filter + + + extension.field-required + + + + + extension.device-name-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.json-name-expression + + + extension.field-required + + + + extension.topic-name-expression + + + extension.field-required + + + + + + + + + + + add + extension.add-connect-request + + + + + + + + + + {{ 'extension.disconnect-requests' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + extension.topic-filter + + + extension.field-required + + + + + extension.device-name-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.json-name-expression + + + extension.field-required + + + + extension.topic-name-expression + + + extension.field-required + + + + + + + + + + + add + extension.add-disconnect-request + + + + + + + + + + {{ 'extension.attribute-requests' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.topic-filter + + + extension.field-required + + + + {{ 'extension.client-scope' | translate }} + + + + + + extension.device-name-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.json-name-expression + + + extension.field-required + + + + extension.topic-name-expression + + + extension.field-required + + + + + + + extension.attribute-key-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.attr-json-key-expression + + + extension.field-required + + + + extension.attr-topic-key-expression + + + extension.field-required + + + + + + + extension.request-id-expression + + + {{value | translate}} + + + + extension.field-required + + + + extension.request-id-json-expression + + + extension.field-required + + + + extension.request-id-topic-expression + + + extension.field-required + + + + + + extension.response-topic-expression + + + extension.field-required + + + + extension.value-expression + + + extension.field-required + + + + + + + + + + add + extension.add-attribute-request + + + + + + + + + + {{ 'extension.attribute-updates' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.device-name-filter + + + extension.field-required + + + + extension.attribute-filter + + + extension.field-required + + + + + extension.topic-expression + + + extension.field-required + + + + extension.value-expression + + + extension.field-required + + + + + + + + + + add + extension.add-attribute-update + + + + + + + + + + {{ 'extension.server-side-rpc' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.device-name-filter + + + extension.field-required + + + + extension.method-filter + + + extension.field-required + + + + + extension.request-topic-expression + + + extension.field-required + + + + extension.response-topic-expression + + + + + extension.response-timeout + + + + extension.value-expression + + + extension.field-required + + + + + + + + + + + add + extension.add-server-side-rpc-request + + + + + + + + + + + + + + + add + extension.add-broker + + + + + + + + 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 0000000000..fc69b68f9b --- /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 0000000000..6d74cc560c --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html @@ -0,0 +1,557 @@ + + + + + extension.configuration + + + + + + + + {{ 'extension.opc-server' | translate }} + + + + + extension.opc-add-server-prompt + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + + + extension.opc-application-name + + + extension.field-required + + + + + + extension.opc-application-uri + + + extension.field-required + + + + + + + + extension.host + + + extension.field-required + + + + + extension.port + + + extension.field-required + extension.port-range + extension.port-range + + + + + + + extension.opc-scan-period-in-seconds + + + extension.field-required + + + + + extension.timeout + + + extension.field-required + + + + + + + + extension.opc-security + + + + + extension.field-required + + + + + extension.opc-identity + + + + + extension.field-required + + + + + + + + + + extension.username + + + extension.field-required + + + + + extension.password + + + extension.field-required + + + + + + + + {{ 'extension.opc-keystore' | translate }} + + + + + extension.opc-keystore-type + + + + + extension.field-required + + + + + + + extension.opc-keystore-location + + + + + {{ 'action.remove' | translate }} + + close + + + + extension.drop-file + + + + + + extension.no-file + {{server.keystore[fieldsToFill.fileName]}} + + + + + + + extension.opc-keystore-password + + + extension.field-required + + + + + extension.opc-keystore-alias + + + extension.field-required + + + + + + extension.opc-keystore-key-password + + + extension.field-required + + + + + + + + + + + + {{ 'extension.mapping' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + + extension.opc-device-node-pattern + + + extension.field-required + + + + + extension.opc-device-name-pattern + + + extension.field-required + + + + + + + + + {{ 'extension.attributes' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + + extension.key + + + extension.field-required + + + + extension.type + + + {{attrTypeValue | translate}} + + + + extension.field-required + + + + + + + extension.value + + + extension.field-required + + + + + + + + + + + + + + add + extension.add-attribute + + + + + + + + + + {{ 'extension.timeseries' | translate }} + + + + + + + + + {{ 'action.remove' | translate }} + + + + + + + extension.key + + + extension.field-required + + + + extension.type + + + {{attrTypeValue | translate}} + + + + extension.field-required + + + + + + extension.value + + + extension.field-required + + + + + + + + + + + add + extension.add-timeseries + + + + + + + + + + + + + + + add + extension.add-map + + + + + + + + + + + + + + add + extension.opc-add-server + + + + + + + + + + \ 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 0000000000..97ac7174c4 --- /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/index.js b/ui/src/app/extension/index.js new file mode 100644 index 0000000000..acb97be60d --- /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 diff --git a/ui/src/app/home/home-links.controller.js b/ui/src/app/home/home-links.controller.js index 273433ba23..b2b93666dd 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; + } + } diff --git a/ui/src/app/home/home-links.scss b/ui/src/app/home/home-links.scss new file mode 100644 index 0000000000..e210334a88 --- /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 diff --git a/ui/src/app/home/home-links.tpl.html b/ui/src/app/home/home-links.tpl.html index 5eac69fe48..f0e558257f 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. --> - - + + @@ -25,12 +25,12 @@ - - - {{place.icon}} - {{place.name}} - - + + + {{place.icon}} + {{place.name}} + + diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index 86d5240953..62e1bb3e2d 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 = {}; diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 9d3e1d16c6..c23b0086a6 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, diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index bd1b9e99b0..ab672de30d 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/CanvasDigitalGauge.js b/ui/src/app/widget/lib/CanvasDigitalGauge.js index a1cfca281d..bd3ff986a8 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); diff --git a/ui/src/app/widget/lib/analogue-compass.js b/ui/src/app/widget/lib/analogue-compass.js index 46059c7582..85ec0a67e6 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 a045f94a8b..da4e3868cc 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 76a2c383aa..7a5c0c7ffe 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 74ab7accdb..231fb6fb2d 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; + } } } } 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 0000000000..3e5ac61741 --- /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 0000000000..26ab5d95dc --- /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 0000000000..067193890c --- /dev/null +++ b/ui/src/app/widget/lib/extensions-table-widget.tpl.html @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/ui/src/index.html b/ui/src/index.html index 2f4045acc9..33702ccd28 100644 --- a/ui/src/index.html +++ b/ui/src/index.html @@ -16,7 +16,7 @@ --> - + diff --git a/ui/src/scss/animations.scss b/ui/src/scss/animations.scss index e7d982081e..ab272528dc 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 diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index 076dbef296..93418b27fe 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; } }