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 @@
+
+
+
+
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
+
+
+
+
+
+
+
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, '');
+ }
+ }
+ 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 @@
+
+
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 @@
+
+
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.",