From e3b39bedf691ef5b1e057c29d2c1fa72d185fa60 Mon Sep 17 00:00:00 2001 From: nickAS21 <44275303+nickAS21@users.noreply.github.com> Date: Thu, 30 Jan 2020 11:11:50 +0200 Subject: [PATCH] WIP_Gate way form (#2370) * gateWayForm: start branch * gateWayForm: start branch2 * gateWayForm: start add new form to gateway_widgets.json * gateWayForm: start add new logs.conf * Fix html and clear js * gateWayForm: start add new222 * improvement gateway config form (change html) * gateWayForm: new vadim verstka * GatewayForm: add valid config * GatewayForm: add valid config compile and add form to widgets library * GatewayForm: bug err yml Co-authored-by: Vladyslav --- .../widget_bundles/gateway_widgets.json | 22 +- ui/package.json | 1 + ui/src/app/common/types.constant.js | 26 + .../gateWay/gateway-config-dialog.tpl.html | 75 +++ .../gateway-config-select.directive.js | 137 +++++ .../gateWay/gateway-config-select.scss | 35 ++ .../gateWay/gateway-config-select.tpl.html | 54 ++ .../gateWay/gateway-config.directive.js | 317 +++++++++++ .../components/gateWay/gateway-config.scss | 85 +++ .../gateWay/gateway-config.tpl.html | 94 ++++ .../gateWay/gateway-form.directive.js | 498 ++++++++++++++++++ .../app/components/gateWay/gateway-form.scss | 40 ++ .../components/gateWay/gateway-form.tpl.html | 219 ++++++++ .../import-export/import-export.service.js | 51 +- ui/src/app/layout/index.js | 6 + ui/src/app/locale/locale.constant-en_US.json | 55 +- 16 files changed, 1712 insertions(+), 3 deletions(-) create mode 100644 ui/src/app/components/gateWay/gateway-config-dialog.tpl.html create mode 100644 ui/src/app/components/gateWay/gateway-config-select.directive.js create mode 100644 ui/src/app/components/gateWay/gateway-config-select.scss create mode 100644 ui/src/app/components/gateWay/gateway-config-select.tpl.html create mode 100644 ui/src/app/components/gateWay/gateway-config.directive.js create mode 100644 ui/src/app/components/gateWay/gateway-config.scss create mode 100644 ui/src/app/components/gateWay/gateway-config.tpl.html create mode 100644 ui/src/app/components/gateWay/gateway-form.directive.js create mode 100644 ui/src/app/components/gateWay/gateway-form.scss create mode 100644 ui/src/app/components/gateWay/gateway-form.tpl.html 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 index c963835ef9..ce78185c29 100644 --- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -20,6 +20,26 @@ "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\":{}}" } + }, + { + "alias": "new_config_form", + "name": "Config form", + "descriptor": { + "type": "static", + "sizeX": 7.5, + "sizeY": 10.5, + "resources": [ + { + "url": "" + } + ], + "templateHtml": "\n\n", + "templateCss": "#container {\n overflow: auto;\n height: 100%;\n margin: auto;\n}\n\n\n\n/*#configurations {*/\n/* display: flex;*/\n/* flex-direction: column;*/\n/* height: 100%;*/\n/* margin: 0px;*/\n/* padding: 0;*/\n/*}*/\n\n/*.configurationPointParent {*/\n/* display: flex;*/\n/* flex-direction: column;*/\n \n/*}*/\n\n/*.configurationPoint {*/\n/* display: flex;*/\n/* flex-direction: row;*/\n/* justify-content: space-between;*/\n/* margin: 5px;*/\n/*}*/\n\n/*.configurationPoint.select {*/\n/* margin: 0px;*/\n/* padding: 0;*/\n/* border: 0;*/\n/* height: 40px;*/\n\n/*}*/\n\n/*.configurationPoint.select.inputRow {*/\n/* margin: 0px;*/\n/* width: 100%;*/\n/* padding: 0;*/\n/* border: 0;*/\n/* height: 40px;*/\n/*}*/\n\n\n/*.error {*/\n/*color: red;*/\n/*}*/", + "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.formId = \"form-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onResize = function() {\n self.ctx.$scope.$broadcast('gateway-form-resize', self.ctx.$scope.formId);\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"GatewayConfigForm\",\n \"properties\": {\n \"gatewayTitle\": {\n \"title\": \"Gateway form title\",\n \"type\": \"string\",\n \"default\": \"Gateway Config Form\"\n }\n }\n },\n \"form\": [\n \"gatewayTitle\"\n ]\n}\n", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"gatewayTitle\":\"Gateway Config Form\"},\"title\":\"Config form\",\"dropShadow\":true,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + } } ] -} \ No newline at end of file +} diff --git a/ui/package.json b/ui/package.json index ccf18eab93..96c94bf677 100644 --- a/ui/package.json +++ b/ui/package.json @@ -61,6 +61,7 @@ "js-beautify": "^1.10.0", "json-schema-defaults": "^0.2.0", "jstree": "^3.3.8", + "jszip": "^3.2.2", "jstree-bootstrap-theme": "^1.0.1", "leaflet": "^1.5.1", "leaflet-polylinedecorator": "^1.6.0", diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 6657caa8fe..e78af1dff7 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -584,6 +584,32 @@ export default angular.module('thingsboard.types', []) opc: "OPC UA", modbus: "MODBUS" }, + gatewayConfigType: { + mqtt: { + value: "mqtt", + name: "MQTT" + }, + modbus: { + value: "modbus", + name: "Modbus" + }, + opc_ua: { + value: "opcua", + name: "OPC-UA" + }, + ble: { + value: "ble", + name: "BLE" + } + }, + gatewayLogLevel: { + none: "NONE", + critical: "CRITICAL", + error: "ERROR", + warning: "WARNING", + info: "INFO", + debug: "DEBUG" + }, extensionValueType: { string: 'value.string', long: 'value.long', diff --git a/ui/src/app/components/gateWay/gateway-config-dialog.tpl.html b/ui/src/app/components/gateWay/gateway-config-dialog.tpl.html new file mode 100644 index 0000000000..ce55d15f48 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config-dialog.tpl.html @@ -0,0 +1,75 @@ + + +
+ +
+

+ gateway.title-connectors-json +

+ + + + +
+
+ +
+
+
+ + + + {{'gateway.tidy'|translate}} + {{'gateway.tidy-tip' | translate }} + + +
+
+
+
+
+ +
+
+
+ + + {{'action.save'|translate}} + + + {{'action.cancel'|translate }} + + +
+
diff --git a/ui/src/app/components/gateWay/gateway-config-select.directive.js b/ui/src/app/components/gateWay/gateway-config-select.directive.js new file mode 100644 index 0000000000..5178fed6d6 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config-select.directive.js @@ -0,0 +1,137 @@ +/* + * Copyright © 2016-2020 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 './gateway-config-select.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import gatewayAliasSelectTemplate from './gateway-config-select.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + + +/* eslint-disable angular/angularelement */ + +export default angular.module('thingsboard.directives.gatewayConfigSelect', []) + .directive('tbGatewayConfigSelect', GatewayConfigSelect) + .name; + +/*@ngInject*/ +function GatewayConfigSelect($compile, $templateCache, $mdConstant, $translate, $mdDialog) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(gatewayAliasSelectTemplate); + element.html(template); + + scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false; + + scope.ngModelCtrl = ngModelCtrl; + scope.singleSelect = null; + + scope.updateValidity = function () { + var value = ngModelCtrl.$viewValue; + var valid = angular.isDefined(value) && value != null || !scope.tbRequired; + ngModelCtrl.$setValidity('singleSelect', valid); + }; + + scope.$watch('singleSelect', function () { + scope.updateView(); + }); + + scope.gatewayNameSearch = function (gatewaySearchText) { + return gatewaySearchText ? scope.gatewayList.filter( + scope.createFilterForGatewayName(gatewaySearchText)) : scope.gatewayList; + }; + + scope.createFilterForGatewayName = function (query) { + var lowercaseQuery = query.toLowerCase(); + return function filterFn(device) { + return (device.toLowerCase().indexOf(lowercaseQuery) === 0); + }; + }; + + scope.updateView = function () { + ngModelCtrl.$setViewValue(scope.singleSelect); + scope.updateValidity(); + let deviceObj = {"name": scope.singleSelect, "type": "Gateway", "additionalInfo": { + "gateway": true + }}; + scope.getAccessToken(deviceObj); + }; + + ngModelCtrl.$render = function () { + if (ngModelCtrl.$viewValue) { + scope.singleSelect = ngModelCtrl.$viewValue; + } + }; + + scope.textIsEmpty = function (str) { + return (!str || 0 === str.length); + }; + + scope.gatewayNameEnter = function ($event) { + if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) { + $event.preventDefault(); + let indexRes = scope.gatewayList.findIndex((element) => element.key === scope.gatewaySearchText); + if (indexRes === -1) { + scope.createNewGatewayDialog($event, {name: scope.gatewaySearchText}); + } + } + }; + + scope.createNewGatewayDialog = function ($event, deviceName) { + if ($event) { + $event.stopPropagation(); + } + var title = $translate.instant('gateway.create-new-gateway'); + var content = $translate.instant('gateway.create-new-gateway-text', {gatewayName: deviceName.name}); + 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( + () => { + let deviceObj = {"name": deviceName.name, "type": "Gateway", "additionalInfo": { + "gateway": true + }}; + scope.createDevice(deviceObj); + }, + () => { + scope.gatewaySearchText = ""; + } + ); + }; + $compile(element.contents())(scope); + }; + + return { + restrict: "E", + require: "^ngModel", + link: linker, + scope: { + tbRequired: '=?', + allowedEntityTypes: '=?', + gatewayList: '=?', + getAccessToken: '=', + createDevice: '=', + theForm: '=' + } + }; +} + +/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/components/gateWay/gateway-config-select.scss b/ui/src/app/components/gateWay/gateway-config-select.scss new file mode 100644 index 0000000000..1c189a7279 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config-select.scss @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2020 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-gateway-autocomplete { + .tb-not-found { + line-height: 1.5; + white-space: normal; + + .tb-no-gateway { + line-height: 48px; + } + } +} + +.tb-gateway-autocomplete-container.md-virtual-repeat-container.md-autocomplete-suggestions-container{ + z-index: 70; +} + +md-autocomplete{ + md-input-container{ + margin-bottom: 0; + } +} diff --git a/ui/src/app/components/gateWay/gateway-config-select.tpl.html b/ui/src/app/components/gateWay/gateway-config-select.tpl.html new file mode 100644 index 0000000000..57c89e5e4a --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config-select.tpl.html @@ -0,0 +1,54 @@ + +
+ + + {{item}} + + +
+
+ gateway.no-gateway-found +
+
+ gateway.no-gateway-matching + gateway.create-new-gateway +
+
+
+
+
Test
+
+
+
diff --git a/ui/src/app/components/gateWay/gateway-config.directive.js b/ui/src/app/components/gateWay/gateway-config.directive.js new file mode 100644 index 0000000000..66ba36620c --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config.directive.js @@ -0,0 +1,317 @@ +/* + * Copyright © 2016-2020 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 './gateway-config.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import gatewayTemplate from './gateway-config.tpl.html'; +import gatewayDialogTemplate from './gateway-config-dialog.tpl.html'; +import beautify from "js-beautify"; + +/* eslint-enable import/no-unresolved, import/default */ +const js_beautify = beautify.js; + +export default angular.module('thingsboard.directives.gatewayConfig', []) + .directive('tbGatewayConfig', GatewayConfig) + .name; + +/*@ngInject*/ +function GatewayConfig() { + return { + restrict: "E", + scope: true, + bindToController: { + disabled: '=ngDisabled', + titleText: '@?', + keyPlaceholderText: '@?', + valuePlaceholderText: '@?', + noDataText: '@?', + gatewayConfig: '=', + changeAlignment: '=' + }, + controller: GatewayConfigController, + controllerAs: 'vm', + templateUrl: gatewayTemplate + }; +} + +/*@ngInject*/ +function GatewayConfigController($scope, $document, $mdDialog, $mdUtil, $window, types, toast, $timeout, $compile, $translate) { //eslint-disable-line + + let vm = this; + + vm.kvList = []; + vm.types = types; + $scope.$watch('vm.gatewayConfig', () => { + vm.stopWatchKvList(); + vm.kvList.length = 0; + if (vm.gatewayConfig) { + for (var property in vm.gatewayConfig) { + if (Object.prototype.hasOwnProperty.call(vm.gatewayConfig, property)) { + vm.kvList.push( + { + enabled: vm.gatewayConfig[property].enabled, + key: property + '', + value: vm.gatewayConfig[property].connector + '', + config: js_beautify(vm.gatewayConfig[property].config + '', {indent_size: 4}) + } + ); + } + } + } + $mdUtil.nextTick(() => { + vm.watchKvList(); + }); + }); + + vm.watchKvList = () => { + $scope.kvListWatcher = $scope.$watch('vm.kvList', () => { + if (!vm.gatewayConfig) { + return; + } + for (let property in vm.gatewayConfig) { + if (Object.prototype.hasOwnProperty.call(vm.gatewayConfig, property)) { + delete vm.gatewayConfig[property]; + } + } + for (let i = 0; i < vm.kvList.length; i++) { + let entry = vm.kvList[i]; + if (entry.key && entry.value) { + let connectorJSON = angular.toJson({ + enabled: entry.enabled, + connector: entry.value, + config: angular.fromJson(entry.config) + }); + vm.gatewayConfig [entry.key] = angular.fromJson(connectorJSON); + } + } + }, true); + }; + + vm.stopWatchKvList = () => { + if ($scope.kvListWatcher) { + $scope.kvListWatcher(); + $scope.kvListWatcher = null; + } + }; + + vm.removeKeyVal = (index) => { + if (index > -1) { + vm.kvList.splice(index, 1); + } + }; + + vm.addKeyVal = () => { + if (!vm.kvList) { + vm.kvList = []; + } + vm.kvList.push( + { + enabled: false, + key: '', + value: '', + config: '{}' + } + ); + } + + vm.openConfigDialog = ($event, index, config, typeName) => { + if ($event) { + $event.stopPropagation(); + } + $mdDialog.show({ + controller: GatewayDialogController, + controllerAs: 'vm', + templateUrl: gatewayDialogTemplate, + parent: angular.element($document[0].body), + locals: { + config: config, + typeName: typeName + }, + targetEvent: $event, + fullscreen: true, + multiple: true, + }).then(function (config) { + if (config) { + if (index > -1) { + vm.kvList[index].config = config; + } + } + }, function () { + }); + + }; + + vm.configTypeChange = (keyVal) => { + for (let prop in types.gatewayConfigType) { + if (types.gatewayConfigType[prop].value === keyVal.value) { + if (!keyVal.key) { + keyVal.key = vm.configTypeChangeValid(types.gatewayConfigType[prop].name, 0); + } + } + } + vm.checkboxValid(keyVal); + }; + + vm.keyValChange = (keyVal, indexKey) => { + keyVal.key = vm.keyValChangeValid(keyVal.key, 0, indexKey); + vm.checkboxValid(keyVal); + }; + + vm.configTypeChangeValid = (name, index) => { + let newKeyName = index ? name + index : name; + let indexRes = vm.kvList.findIndex((element) => element.key === newKeyName); + return indexRes === -1 ? newKeyName : vm.configTypeChangeValid(name, ++index); + }; + + vm.keyValChangeValid = (name, index, indexKey) => { + angular.forEach(vm.kvList, function (value, key) { + let nameEq = (index === 0) ? name : name + index; + if (key !== indexKey && value.key && value.key === nameEq) { + index++; + vm.keyValChangeValid(name, index, indexKey); + } + + }); + return (index === 0) ? name : name + index; + }; + + vm.buttonValid = (config) => { + return (angular.equals("{}", config)) ? "md-warn" : "md-primary"; + }; + + vm.checkboxValid = (keyVal) => { + if (!keyVal.key || angular.equals("", keyVal.key) + || !keyVal.value || angular.equals("", keyVal.value) + || angular.equals("{}", keyVal.config)) { + return keyVal.enabled = false; + } + return true; + }; + vm.checkboxValidMouseover = ($event, keyVal) => { + console.log($event, keyVal); //eslint-disable-line + vm.checkboxValidClick ($event, keyVal); + }; + + vm.checkboxValidClick = ($event, keyVal) => { + if (!vm.checkboxValid(keyVal)) { + let errTxt = ""; + if (!keyVal.key || angular.equals("", keyVal.key)) { + errTxt = $translate.instant('gateway.keyval-name-err'); + } + + if (!keyVal.value || angular.equals("", keyVal.value)) { + errTxt += '
' + $translate.instant('gateway.keyval-type-err') + '
'; + } + + if (angular.equals("{}", keyVal.config)) { + errTxt += '
' + $translate.instant('gateway.keyval-config-err') + '
'; + } + if (!angular.equals("", errTxt)) { + displayTooltip($event, '
' + + '
' + + '
' + $translate.instant('gateway.keyval-save-err') + '
' + + '
' + errTxt + '
' + + '
' + + '
'); + } + } + else { + destroyTooltips(); + } + }; + + + function displayTooltip(event, content) { + destroyTooltips(); + vm.tooltipTimeout = $timeout(() => { + var element = angular.element(event.target); + element.tooltipster( + { + theme: 'tooltipster-shadow', + delay: 10, + animation: 'grow', + side: 'right' + } + ); + var contentElement = angular.element(content); + $compile(contentElement)($scope); + var tooltip = element.tooltipster('instance'); + tooltip.content(contentElement); + tooltip.open(); + }, 500); + } + + function destroyTooltips() { + if (vm.tooltipTimeout) { + $timeout.cancel(vm.tooltipTimeout); + vm.tooltipTimeout = null; + } + var instances = angular.element.tooltipster.instances(); + instances.forEach((instance) => { + if (!instance.isErrorTooltip) { + instance.destroy(); + } + }); + } +} + +/*@ngInject*/ +function GatewayDialogController($scope, $mdDialog, $document, $window, config, typeName) { + let vm = this; + vm.doc = $document[0]; + vm.config = angular.copy(config); + vm.typeName = "" + typeName; + vm.configAreaOptions = { + useWrapMode: false, + mode: 'json', + showGutter: true, + showPrintMargin: true, + theme: 'github', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function (_ace) { + _ace.$blockScrolling = 1; + } + }; + + vm.validateConfig = (model, editorName) => { + if (model && model.length) { + try { + angular.fromJson(model); + $scope.theForm[editorName].$setValidity('configJSON', true); + } catch (e) { + $scope.theForm[editorName].$setValidity('configJSON', false); + } + } + }; + + vm.save = () => { + $mdDialog.hide(vm.config); + }; + + vm.cancel = () => { + $mdDialog.hide(); + }; + + vm.beautifyJson = () => { + vm.config = js_beautify(vm.config, {indent_size: 4}); + }; +} + diff --git a/ui/src/app/components/gateWay/gateway-config.scss b/ui/src/app/components/gateWay/gateway-config.scss new file mode 100644 index 0000000000..f128db8f21 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config.scss @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2020 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. + */ +.gateway-config { + span.no-data-found { + position: relative; + display: flex; + height: 40px; + text-transform: uppercase; + + &.disabled { + color: rgba(0, 0, 0, .38); + } + } + + .gateway-config-row{ + md-input-container{ + margin-bottom: 0; + } + + &.gateway-config-row-vertical { + flex-direction: column; + } + } + + .action-buttons.gateway-config-row-vertical { + flex-direction: column; + justify-content: space-evenly; + } +} + +.gateway-config-dialog{ + .md-button.tidy{ + min-width: 32px; + min-height: 15px; + padding: 4px; + margin: 0 5px 0 0; + font-size: .8rem; + line-height: 15px; + color: #7b7b7b; + background: rgba(220, 220, 220, .35); + } + + .tb-json-toolbar{ + height: 40px; + } + + .tb-json-panel { + height: calc(100% - 80px); + margin-left: 15px; + border: 1px solid #c0c0c0; + + .tb-json-input { + width: 100%; + min-width: 400px; + height: 100%; + + &:not(.fill-height) { + min-height: 200px; + } + } + } +} + +@media (max-width: 425px){ + .gateway-config-dialog{ + .tb-json-panel { + .tb-json-input { + min-width: 200px; + } + } + } +} diff --git a/ui/src/app/components/gateWay/gateway-config.tpl.html b/ui/src/app/components/gateWay/gateway-config.tpl.html new file mode 100644 index 0000000000..565c771099 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-config.tpl.html @@ -0,0 +1,94 @@ + +
+
+
+ + + + + {{ 'gateway.enabled' | translate }} + + +
+
+ + + + + {{configType.value}} + + + + {{ 'gateway.connector-type' | translate }} + + + + +
+
extension.field-required
+
+ + {{ 'gateway.name' | translate }} + +
+
+
+ + settings_ethernet + + {{ 'gateway.update-config' | translate }} + + + + close + + {{ 'gateway.delete' | translate }} + + +
+
+ {{vm.noDataText ? vm.noDataText : 'gateway.no-connectors'}} +
+ + + {{ 'gateway.add-connectors' | translate }} + + action.add + +
+
diff --git a/ui/src/app/components/gateWay/gateway-form.directive.js b/ui/src/app/components/gateWay/gateway-form.directive.js new file mode 100644 index 0000000000..2aa05ce004 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-form.directive.js @@ -0,0 +1,498 @@ +/* + * Copyright © 2016-2020 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 './gateway-form.scss'; +/* eslint-disable import/no-unresolved, import/default */ + +import gatewayFormTemplate from './gateway-form.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +export default angular.module('thingsboard.directives.gatewayForm', []) + .directive('tbGatewayForm', GatewayForm) + .name; + +/*@ngInject*/ +function GatewayForm() { + return { + restrict: "E", + scope: true, + bindToController: { + disabled: '=ngDisabled', + keyPlaceholderText: '@?', + valuePlaceholderText: '@?', + noDataText: '@?', + formId: '=', + ctx: '=', + gatewayFormConfig: '=', + theForm: '=' + }, + controller: GatewayFormController, + controllerAs: 'vm', + templateUrl: gatewayFormTemplate + }; +} + +/*@ngInject*/ +function GatewayFormController($scope, $injector, $document, $mdExpansionPanel, toast, importExport, attributeService, deviceService, userService, $mdDialog, $mdUtil, types, $window, $q) { + $scope.$mdExpansionPanel = $mdExpansionPanel; + let vm = this; + const attributeNameClinet = "current_configuration"; + const attributeNameServer = "configuration_drafts"; + const attributeNameShared = "configuration"; + const attributeNameLogShared = "RemoteLoggingLevel"; + vm.remoteLoggingConfig = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"'; + vm.types = types; + + vm.configurations = { + singleSelect: '', + host: $document[0].domain, + port: 1883, + remoteConfiguration: true, + accessToken: '', + entityType: '', + entityId: '', + storageType: "memoryStorage", // "memoryStorage"; fileStorage + readRecordsCount: 100, + maxRecordsCount: 10000, + dataFolderPath: './data/', + maxFilesCount: 5, + securityType: "accessToken", // "accessToken", "tls" + caCertPath: '/etc/thingsboard-gateway/ca.pem', + privateKeyPath: '/etc/thingsboard-gateway/privateKey.pem', + certPath: '/etc/thingsboard-gateway/certificate.pem', + connectors: {}, + remoteLoggingLevel: "DEBUG", // level login + remoteLoggingPathToLogs: './logs/' + }; + getGatewaysListByUser(true); + + vm.securityTypes = [{ + name: 'Access Token', + value: 'accessToken' + }, { + name: 'TLS', + value: 'tls' + }]; + + vm.storageTypes = [{ + name: 'Memory storage', + value: 'memoryStorage' + }, { + name: 'File storage', + value: 'fileStorage' + }]; + + $scope.$on('gateway-form-resize', function (event, formId) { + if (vm.formId == formId) { + updateWidgetDisplaying(); + } + }); + + function updateWidgetDisplaying() { + if (vm.ctx && vm.ctx.$container) { + vm.changeAlignment = (vm.ctx.$container[0].offsetWidth <= 425); + } + } + + updateWidgetDisplaying(); + + vm.getAccessToken = (deviceObj) => { + if (deviceObj.name) { + deviceService.findByName(deviceObj.name, {ignoreErrors: true}) + .then( + function (device) { + getDeviceCredential(device.id.id); + } + ) + } + }; + + function getDeviceCredential(deviceId) { + return deviceService.getDeviceCredentials(deviceId).then( + (deviceCredentials) => { + vm.configurations.accessToken = deviceCredentials.credentialsId; + vm.configurations.entityType = deviceCredentials.deviceId.entityType; + vm.configurations.entityId = deviceCredentials.deviceId.id; + vm.getAttributeStart(); + } + ); + } + + vm.createDevice = (deviceObj) => { + deviceService.findByName(deviceObj.name, {ignoreErrors: true}) + .then( + function (device) { + getDeviceCredential(device.id.id).then(() => { + getGatewaysListByUser(); + }); + }, + function () { + deviceService.saveDevice(deviceObj).then( + (device) => { + deviceService.getDeviceCredentials(device.id.id).then( + (data) => { + vm.configurations.accessToken = data.credentialsId; + vm.configurations.entityType = device.id.entityType; + vm.configurations.entityId = device.id.id; + vm.getAttributeStart(); + getGatewaysListByUser(); + } + ); + } + ); + }); + }; + + vm.saveAttributeConfig = () => { + vm.setAttribute(attributeNameShared, $window.btoa(angular.toJson(vm.getConfigAllByAttributeJSON())), types.attributesScope.shared.value); + vm.setAttribute(attributeNameServer, $window.btoa(angular.toJson(vm.getConfigByAttributeTmpJSON())), types.attributesScope.server.value); + vm.setAttribute(attributeNameLogShared, vm.configurations.remoteLoggingLevel.toUpperCase(), types.attributesScope.shared.value); + }; + + vm.getAttributeStart = () => { + let initResps = []; + vm.configurations.connectors = {}; + initResps.push(vm.getAttributeConfig(attributeNameClinet, types.attributesScope.client.value)); + initResps.push(vm.getAttributeConfig(attributeNameServer, types.attributesScope.server.value)); + initResps.push(vm.getAttributeConfig(attributeNameLogShared, types.attributesScope.shared.value)); + $q.all(initResps).then((resp) => { + vm.getAttributeInitFromClient(resp[0]); + vm.getAttributeInitFromServer(resp[1]); + vm.getAttributeInitFromShared(resp[2]); + }, (err) => { + console.log("getAttribute_error", err); //eslint-disable-line + }); + }; + + vm.getAttributeConfig = (attributeName, typeValue) => { + let keys = [attributeName]; + return attributeService.getEntityAttributesValues(vm.configurations.entityType, vm.configurations.entityId, typeValue, keys); + }; + + vm.setAttribute = (attributeName, attributeConfig, typeValue) => { + let attributes = [ + { + key: attributeName, + value: attributeConfig + } + ]; + attributeService.saveEntityAttributes(vm.configurations.entityType, vm.configurations.entityId, typeValue, attributes).then(() => { + }, (err) => { + console.log("setAttribute_", err); //eslint-disable-line + }); + }; + + vm.exportConfig = () => { + let fileZip = {}; + fileZip["tb_gateway.yaml"] = vm.getConfig(); + vm.createConfigByExport(fileZip); + vm.getLogsConfigByExport(fileZip); + importExport.exportJSZip(fileZip, 'config'); + vm.setAttribute(attributeNameLogShared, vm.configurations.remoteLoggingLevel.toUpperCase(), types.attributesScope.shared.value); + }; + + vm.getConfig = () => { + let config; + config = 'thingsboard:\n'; + config += ' host: ' + vm.configurations.host + '\n'; + config += ' remoteConfiguration: ' + vm.configurations.remoteConfiguration + '\n'; + config += ' port: ' + vm.configurations.port + '\n'; + config += ' security:\n'; + if (vm.configurations.securityType === 'accessToken') { + config += ' access-token: ' + vm.configurations.accessToken + '\n'; + } else if (vm.configurations.securityType === 'tls') { + config += ' ca_cert: ' + vm.configurations.caCertPath + '\n'; + config += ' privateKey: ' + vm.configurations.privateKeyPath + '\n'; + config += ' cert: ' + vm.configurations.certPath + '\n'; + } + config += 'storage:\n'; + if (vm.configurations.storageType === 'memoryStorage') { + config += ' type: memory\n'; + config += ' read_records_count: ' + vm.configurations.readRecordsCount + '\n'; + config += ' max_records_count: ' + vm.configurations.maxRecordsCount + '\n'; + } else if (vm.configurations.storageType === 'fileStorage') { + config += ' type: file\n'; + config += ' data_folder_path: ' + vm.configurations.dataFolderPath + '\n'; + config += ' max_file_count: ' + vm.configurations.maxFilesCount + '\n'; + config += ' max_read_records_count: ' + vm.configurations.readRecordsCount + '\n'; + config += ' max_records_per_file: ' + vm.configurations.maxRecordsCount + '\n'; + } + config += 'connectors:\n'; + for (let connector in vm.configurations.connectors) { + if (vm.configurations.connectors[connector].enabled) { + config += ' -\n'; + config += ' name: ' + connector + ' Connector\n'; + config += ' type: ' + vm.configurations.connectors[connector].connector + '\n'; + config += ' configuration: ' + vm.validFileName(connector) + ".json" + '\n'; + } + } + return config; + }; + + vm.createConfigByExport = (fileZipAdd) => { + for (let connector in vm.configurations.connectors) { + if (vm.configurations.connectors[connector].enabled) { + fileZipAdd[vm.validFileName(connector) + ".json"] = angular.toJson(vm.configurations.connectors[connector].config); + } + } + }; + + vm.getLogsConfigByExport = (fileZipAdd) => { + fileZipAdd["logs.conf"] = vm.getLogsConfig(); + }; + + vm.getLogsConfig = () => { + return vm.remoteLoggingConfig + .replace(/{ERROR}/g, vm.configurations.remoteLoggingLevel) + .replace(/{.\/logs\/}/g, vm.configurations.remoteLoggingPathToLogs); + }; + + vm.getConfigAllByAttributeJSON = () => { + let thingsBoardAll = {}; + thingsBoardAll["thingsboard"] = vm.getConfigMainByAttributeJSON(); + vm.getConfigByAttributeJSON(thingsBoardAll); + return thingsBoardAll; + }; + + vm.getConfigMainByAttributeJSON = () => { + let configMain = {}; + let thingsBoard = {}; + thingsBoard.host = vm.configurations.host; + thingsBoard.remoteConfiguration = vm.configurations.remoteConfiguration; + thingsBoard.port = vm.configurations.port; + let security = {}; + if (vm.configurations.securityType === 'accessToken') { + security.accessToken = (vm.configurations.accessToken) ? vm.configurations.accessToken : "" + } else { + security.caCert = vm.configurations.caCertPath; + security.privateKey = vm.configurations.privateKeyPath; + security.cert = vm.configurations.certPath; + } + thingsBoard.security = security; + configMain.thingsboard = thingsBoard; + + let storage = {}; + if (vm.configurations.storageType === 'memoryStorage') { + storage.type = "memory"; + storage.read_records_count = vm.configurations.readRecordsCount; + storage.max_records_count = vm.configurations.maxRecordsCount; + } else if (vm.configurations.storageType === 'fileStorage') { + storage.type = "file"; + storage.data_folder_path = vm.configurations.dataFolderPath; + storage.max_file_count = vm.configurations.maxFilesCount; + storage.max_read_records_count = vm.configurations.readRecordsCount; + storage.max_records_per_file = vm.configurations.maxRecordsCount; + } + configMain.storage = storage; + + let conn = []; + for (let connector in vm.configurations.connectors) { + if (vm.configurations.connectors[connector].enabled) { + let connect = {}; + connect.configuration = vm.validFileName(connector) + ".json"; + connect.name = connector; + connect.type = vm.configurations.connectors[connector].connector; + conn.push(connect); + } + } + configMain.connectors = conn; + + configMain.logs = $window.btoa(vm.getLogsConfig()); + + return configMain; + }; + + vm.getConfigByAttributeJSON = (thingsBoardBy) => { + for (let connector in vm.configurations.connectors) { + if (vm.configurations.connectors[connector].enabled) { + let typeAr = vm.configurations.connectors[connector].connector; + let objTypeAll = []; + for (let conn in vm.configurations.connectors) { + if (typeAr === vm.configurations.connectors[conn].connector && vm.configurations.connectors[conn].enabled) { + let objType = {}; + objType["name"] = conn; + objType["config"] = vm.configurations.connectors[conn].config; + objTypeAll.push(objType); + } + } + if (objTypeAll.length > 0) { + thingsBoardBy[typeAr] = objTypeAll; + } + } + } + }; + + vm.getConfigByAttributeTmpJSON = () => { + let connects = {}; + for (let connector in vm.configurations.connectors) { + if (!vm.configurations.connectors[connector].enabled && Object.keys(vm.configurations.connectors[connector].config).length !== 0) { + let conn = {}; + conn["connector"] = vm.configurations.connectors[connector].connector; + conn["config"] = vm.configurations.connectors[connector].config; + connects[connector] = conn; + } + } + return connects; + }; + + function getGatewaysListByUser(firstInit) { + vm.gateways = []; + vm.currentUser = userService.getCurrentUser(); + if (vm.currentUser.authority === 'TENANT_ADMIN') { + deviceService.getTenantDevices({limit: 500}).then( + (devices) => { + if (devices.data.length > 0) { + devices.data.forEach((device) => { + if (device.additionalInfo !== null && device.additionalInfo.gateway === true) { + vm.gateways.push(device.name); + if (firstInit && vm.gateways.length && device.name === vm.gateways[0]) { + vm.configurations.singleSelect = vm.gateways[0]; + let deviceObj = { + "name": vm.configurations.singleSelect, + "type": "Gateway", + "additionalInfo": { + "gateway": true + } + }; + vm.getAccessToken(deviceObj); + } + } + }); + } + } + ); + } else if (vm.currentUser.authority === 'CUSTOMER_USER') { + deviceService.getCustomerDevices(vm.currentUser.customerId, {limit: 500}).then( + (devices) => { + if (devices.data.length > 0) { + devices.data.forEach((device) => { + if (device.additionalInfo !== null && device.additionalInfo.gateway === true) { + vm.gateways.push(device.name); + if (firstInit && vm.gateways.length) { + vm.configurations.singleSelect = vm.gateways[0]; + let deviceObj = { + "name": vm.configurations.singleSelect, + "type": "Gateway", + "additionalInfo": { + "gateway": true + } + }; + vm.getAccessToken(deviceObj); + } + } + }); + } + } + ); + } + } + + vm.getAttributeInitFromClient = (resp) => { + if (resp.length > 0) { + vm.configurations.connectors = {}; + let attribute = angular.fromJson($window.atob(resp[0].value)); + for (var type in attribute) { + let keyVal = attribute[type]; + if (type === "thingsboard") { + if (keyVal !== null && Object.keys(keyVal).length > 0) { + vm.setConfigMain(keyVal); + } + } else { + for (let typeVal in keyVal) { + let typeName = ''; + if (Object.prototype.hasOwnProperty.call(keyVal[typeVal], 'name')) { + typeName = 'name'; + } + let key = ""; + key = (typeName === "") ? "No name" : ((typeName === 'name') ? keyVal[typeVal].name : keyVal[typeVal][typeName].name); + let conn = {}; + conn["enabled"] = true; + conn["connector"] = type; + conn["config"] = angular.toJson(keyVal[typeVal].config); + vm.configurations.connectors[key] = conn; + } + } + } + } + }; + + vm.getAttributeInitFromServer = (resp) => { + if (resp.length > 0) { + let attribute = angular.fromJson($window.atob(resp[0].value)); + for (let key in attribute) { + let conn = {}; + conn["enabled"] = false; + conn["connector"] = attribute[key].connector; + conn["config"] = angular.toJson(attribute[key].config); + vm.configurations.connectors[key] = conn; + } + } + }; + + vm.getAttributeInitFromShared = (resp) => { + if (resp.length > 0) { + if (vm.types.gatewayLogLevel[resp[0].value.toLowerCase()]) { + vm.configurations.remoteLoggingLevel = resp[0].value.toUpperCase(); + } + } else { + vm.configurations.remoteLoggingLevel = vm.types.gatewayLogLevel.debug; + } + }; + + vm.setConfigMain = (keyVal) => { + if (Object.prototype.hasOwnProperty.call(keyVal, 'thingsboard')) { + vm.configurations.host = keyVal.thingsboard.host; + vm.configurations.port = keyVal.thingsboard.port; + vm.configurations.remoteConfiguration = keyVal.thingsboard.remoteConfiguration; + if (Object.prototype.hasOwnProperty.call(keyVal.thingsboard.security, 'accessToken')) { + vm.configurations.securityType = 'accessToken'; + vm.configurations.accessToken = keyVal.thingsboard.security.accessToken; + } else { + vm.configurations.securityType = 'tls'; + vm.configurations.caCertPath = keyVal.thingsboard.security.caCert; + vm.configurations.privateKeyPath = keyVal.thingsboard.security.private_key; + vm.configurations.certPath = keyVal.thingsboard.security.cert; + } + } + if (Object.prototype.hasOwnProperty.call(keyVal, 'storage') && Object.prototype.hasOwnProperty.call(keyVal.storage, 'type')) { + if (keyVal.storage.type === 'memory') { + vm.configurations.storageType = 'memoryStorage'; + vm.configurations.readRecordsCount = keyVal.storage.read_records_count; + vm.configurations.maxRecordsCount = keyVal.storage.max_records_count; + } else if (keyVal.storage.type === 'file') { + vm.configurations.storageType = 'fileStorage'; + vm.configurations.dataFolderPath = keyVal.storage.data_folder_path; + vm.configurations.maxFilesCount = keyVal.storage.max_file_count; + vm.configurations.readRecordsCount = keyVal.storage.read_records_count; + vm.configurations.maxRecordsCount = keyVal.storage.max_records_count; + } + } + }; + + vm.setSaveTypeConfig = (itemVal) => { + vm.configurations.remoteConfiguration = itemVal.item; + }; + + vm.validFileName = (fileName) => { + let fileName1 = fileName.replace("_", ""); + let fileName2 = fileName1.replace("-", ""); + let fileName3 = fileName2.replace(/^\s+|\s+$/g, ''); + let fileName4 = fileName3.toLowerCase(); + return fileName4; + }; +} + + diff --git a/ui/src/app/components/gateWay/gateway-form.scss b/ui/src/app/components/gateWay/gateway-form.scss new file mode 100644 index 0000000000..a5e7bc8b44 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-form.scss @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2020 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. + */ +.gateway-form{ + padding: 5px 5px 0; + + .gateway-form-row{ + md-input-container{ + margin-bottom: 0; + } + + &.gateway-config-row-vertical{ + flex-direction: column; + + .md-select-container{ + margin-bottom: 14px; + } + } + } + + .form-action-buttons{ + padding-top: 8px; + } +} + +.security-type { + margin-top: 38px; +} diff --git a/ui/src/app/components/gateWay/gateway-form.tpl.html b/ui/src/app/components/gateWay/gateway-form.tpl.html new file mode 100644 index 0000000000..8ea0cfcc07 --- /dev/null +++ b/ui/src/app/components/gateWay/gateway-form.tpl.html @@ -0,0 +1,219 @@ + +
+ + + +
{{ 'gateway.thingsboard' | translate | uppercase }}
+ + +
+ + +
{{ 'gateway.thingsboard' | translate | uppercase }}
+ + +
+ + + + + + + + {{securityType.name}} + + + +
+ + + +
+
extension.field-required
+
+
+ + + +
+
extension.field-required
+
max
+
min
+
+
+
+
+ + + + + + + + + + + + +
+ + {{ 'gateway.remote' | translate }} + {{'gateway.remote-tip' | translate }} + +
+ + + + + {{loggingLevel}} + + + + + + +
+
extension.field-required
+
+
+
+
+
+
+ + +
{{ 'gateway.storage' | translate | uppercase }}
+ + +
+ + +
{{ 'gateway.storage' | translate | uppercase }}
+ + +
+ + + + + + {{storageType.name}} + + + + +
+ + + +
+
extension.field-required
+
+
+ + + + +
+
extension.field-required
+
+
+
+ +
+ + + +
+
extension.field-required
+
+
+ + + + +
+
extension.field-required
+
+
+
+
+
+
+ + +
{{ 'gateway.connectors' | translate | uppercase }}
+ + +
+ + +
{{ 'gateway.connectors' | translate | uppercase }}
+ + +
+ + + + +
+
+
+
+ + {{'action.download' | translate }} + {{'gateway.download-tip' | translate }} + + + + {{'action.save' | translate }} + {{'gateway.save-tip' | translate }} + +
+
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index cdc9b985b8..6220fc3c6a 100644 --- a/ui/src/app/import-export/import-export.service.js +++ b/ui/src/app/import-export/import-export.service.js @@ -18,6 +18,7 @@ import importDialogTemplate from './import-dialog.tpl.html'; import importDialogCSVTemplate from './import-dialog-csv.tpl.html'; import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; +import * as JSZip from 'jszip'; /* eslint-enable import/no-unresolved, import/default */ @@ -28,6 +29,10 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, $rootScope, dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) { + const JSZIP_TYPE = { + mimeType: 'application/zip', + extension: 'zip' + }; var service = { exportDashboard: exportDashboard, @@ -40,6 +45,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, importWidgetType: importWidgetType, exportWidgetsBundle: exportWidgetsBundle, importWidgetsBundle: importWidgetsBundle, + exportJSZip: exportJSZip, exportExtension: exportExtension, importExtension: importExtension, importEntities: importEntities, @@ -851,7 +857,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, }); return $q.all(promises); } - + function createMultiEntity(arrayData, entityType, updateData, config) { let partSize = 100; partSize = arrayData.length > partSize ? partSize : arrayData.length; @@ -982,6 +988,49 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, let dialogElement = element[0].getElementsByTagName('md-dialog'); dialogElement[0].style.width = dialogElement[0].offsetWidth + 2 + "px"; } + + /** + * + * @param data + * @param filename + * Warn data !!! Not object, if object, then object convert from object to format txt + * Example: data = {keyNameFile1: valueFile1, + * keyNameFile2: valueFile2...} + * fileName - name file of the arhiv + */ + function exportJSZip(data, filename) { + let jsZip = new JSZip(); + for (let keyName in data) { + let valueData = data[keyName]; + jsZip.file(keyName, valueData); + } + jsZip.generateAsync({type: "Blob"}).then(function (content) { + downloadFile(content, filename, JSZIP_TYPE); + }); + } + + + function downloadFile(data, filename, fileType) { + console.log("downloadFile", data, filename, fileType); // eslint-disable-line + if (!filename) { + filename = 'download'; + } + filename += '.' + fileType.extension; + var blob = new Blob([data], {type: fileType.mimeType}); + // FOR IE: + if (window.navigator && window.navigator.msSaveOrOpenBlob) { + window.navigator.msSaveOrOpenBlob(blob, filename); + } else { + var e = document.createEvent('MouseEvents'), + a = document.createElement('a'); + a.download = filename; + a.href = window.URL.createObjectURL(blob); + a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); + e.initEvent('click', true, false, window, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + a.dispatchEvent(e); + } + } } /* eslint-enable no-undef, angular/window-service, angular/document-service */ diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 84f89adb1b..d674a1fdd3 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -30,6 +30,9 @@ import thingsboardSideMenu from '../components/side-menu.directive'; import thingsboardNavTree from '../components/nav-tree.directive'; import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive'; import thingsboardKvMap from '../components/kv-map.directive'; +import thingsboardGatewayConfig from '../components/gateWay/gateway-config.directive'; +import thingsboardGatewayConfigSelect from '../components/gateWay/gateway-config-select.directive'; +import thingsboardGatewayForm from '../components/gateWay/gateway-form.directive'; import thingsboardJsonObjectEdit from '../components/json-object-edit.directive'; import thingsboardJsonContent from '../components/json-content.directive'; @@ -93,6 +96,9 @@ export default angular.module('thingsboard.home', [ thingsboardNavTree, thingsboardDashboardAutocomplete, thingsboardKvMap, + thingsboardGatewayConfig, + thingsboardGatewayConfigSelect, + thingsboardGatewayForm, thingsboardJsonObjectEdit, thingsboardJsonContent ]) diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 37caf9875b..5acd0f81e5 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -50,7 +50,8 @@ "export": "Export", "share-via": "Share via {{provider}}", "continue": "Continue", - "discard-changes": "Discard Changes" + "discard-changes": "Discard Changes", + "download": "Download" }, "aggregation": { "aggregation": "Aggregation", @@ -1124,6 +1125,58 @@ "function": { "function": "Function" }, + "gateway": { + "key": "Key configuration", + "value": "Value configuration", + "remove-entry": "Remove configuration", + "add-entry": "Add configuration", + "no-data": "No configurations", + "gateway-required": "Gateway is required.", + "gateway-name": "Gateway name", + "create-new-gateway": "Create a new gateway", + "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", + "no-gateway-matching": " '{{item}}' not found.", + "thingsboard": "ThingsBoard", + "connectors": "Connectors configuration", + "thingsboard-host": "ThingsBoard Host", + "thingsboard-port": "ThingsBoard Port", + "security-type": "Security type", + "tls-path-ca-certificate": "Path to CA certificate on gateway:", + "tls-path-private-key": "Path to private key on gateway:", + "tls-path-client-certificate": "Path to client certificate on gateway:", + "storage": "Storage", + "storage-type": "Storage type", + "storage-read-time": "Read records per time:", + "storage-max-time": "Maximum records per time:", + "storage-max-files": "Maximum files:", + "storage-data-path": "Data folder path:", + "download-tip": "Download configuration file", + "save-tip": "Save configuration file", + "remote-tip": "Allow remote configuration", + "remote": "Remote configuration", + "remote-logging-level": "Logging level", + "remote-logging-path-logs": "Path to logs", + "connector-type": "Connector type", + "update-config": "Add/update config JSON", + "delete": "Delete configuration", + "title-connectors-json": "Connector {{typeName}} configuration", + "json-required": "Config json is required for gateway config.", + "json-parse": "Unable to parse config json for gateway config.", + "tidy": "Tidy", + "tidy-tip": "Tidy config JSON", + "transformer-json-config": "JSON for the config*", + "toggle-fullscreen": "Toggle fullscreen", + "add-connectors": "Add new connectors", + "no-connectors": "No connectors", + "enabled": "Enabled", + "name": "Name", + "no-gateway-found": "No gateway found.", + "gateway": "Gateway", + "keyval-save-err": "Save config error", + "keyval-name-err": "Please add Name", + "keyval-type-err": "Please add Connector type", + "keyval-config-err": "Please add configuration JSON" + }, "grid": { "delete-item-title": "Are you sure you want to delete this item?", "delete-item-text": "Be careful, after the confirmation this item and all related data will become unrecoverable.",