diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js index 7f235c9d89..6ed2921824 100644 --- a/ui/src/app/api/attribute.service.js +++ b/ui/src/app/api/attribute.service.js @@ -238,23 +238,23 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) function success() { deferred.resolve(response.data); }, - function fail(response) { - deferred.reject(response); + function fail() { + deferred.reject(); } ) } else { deferred.resolve(response.data); } - }, function fail(response) { - deferred.reject(response); + }, function fail() { + deferred.reject(); }); } else if (deleteEntityAttributesPromise) { deleteEntityAttributesPromise.then( function success() { deferred.resolve(); }, - function fail(response) { - deferred.reject(response); + function fail() { + deferred.reject(); } ) } else { @@ -287,23 +287,23 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) function success() { deferred.resolve(response.data); }, - function fail(response) { - deferred.reject(response); + function fail() { + deferred.reject(); } ) } else { deferred.resolve(response.data); } - }, function fail(response) { - deferred.reject(response); + }, function fail() { + deferred.reject(); }); } else if (deleteEntityTimeseriesPromise) { deleteEntityTimeseriesPromise.then( function success() { deferred.resolve(); }, - function fail(response) { - deferred.reject(response); + function fail() { + deferred.reject(); } ) } else { @@ -325,8 +325,8 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys; $http.delete(url, config).then(function success() { deferred.resolve(); - }, function fail(response) { - deferred.reject(response); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -344,8 +344,8 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/delete' + '?keys=' + keys; $http.delete(url, config).then(function success() { deferred.resolve(); - }, function fail(response) { - deferred.reject(response); + }, function fail() { + deferred.reject(); }); return deferred.promise; } diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index c99376e17f..e3f9630193 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -43,8 +43,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom sendOneWayRpcCommand: sendOneWayRpcCommand, sendTwoWayRpcCommand: sendTwoWayRpcCommand, findByQuery: findByQuery, - getDeviceTypes: getDeviceTypes, - findByName: findByName + getDeviceTypes: getDeviceTypes } return service; @@ -167,8 +166,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom var url = '/api/device'; $http.post(url, device, config).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response); + }, function fail() { + deferred.reject(); }); return deferred.promise; } @@ -178,18 +177,36 @@ function DeviceService($http, $q, $window, userService, attributeService, custom let attributesType = Object.keys(types.attributesScope); let allPromise = []; let promise = ""; + let statisticalInfo = {}; for (let i = 0; i < attributesType.length; i++) { - if (deviceRelation.attributes[attributesType[i]] && deviceRelation.attributes[attributesType[i]].length !== 0) { - promise = attributeService.saveEntityAttributes(types.entityType.device, deviceId, types.attributesScope[attributesType[i]].value, deviceRelation.attributes[attributesType[i]], config); + let attrribute = attributesType[i]; + if (deviceRelation.attributes[attrribute] && deviceRelation.attributes[attrribute].length !== 0) { + promise = attributeService.saveEntityAttributes(types.entityType.device, deviceId, types.attributesScope[attrribute].value, deviceRelation.attributes[attrribute], config).then(function () { + statisticalInfo.create = { + [attrribute]: deviceRelation.attributes[attributesType[i]].length + }; + }, function () { + statisticalInfo.error = { + [attrribute]: deviceRelation.attributes[attributesType[i]].length + }; + }); allPromise.push(promise); } } if (deviceRelation.timeseries.length !== 0) { - promise = attributeService.saveEntityTimeseries(types.entityType.device, deviceId, "time", deviceRelation.timeseries, config); + promise = attributeService.saveEntityTimeseries(types.entityType.device, deviceId, "time", deviceRelation.timeseries, config).then(function () { + statisticalInfo.create = { + timeseries: deviceRelation.timeseries.length + }; + }, function () { + statisticalInfo.error = { + timeseries: deviceRelation.timeseries.length + }; + }); allPromise.push(promise); } $q.all(allPromise).then(function success() { - deferred.resolve(); + deferred.resolve(statisticalInfo); }); return deferred.promise; } @@ -197,26 +214,28 @@ function DeviceService($http, $q, $window, userService, attributeService, custom function saveDeviceParameters(deviceParameters, update, config) { config = config || {}; const deferred = $q.defer(); - let statisticalInfo = { - create: {}, - update: {}, - error: {} - }; + let statisticalInfo = {}; let newDevice = { name: deviceParameters.name, type: deviceParameters.type }; saveDevice(newDevice, config).then(function success(response) { - statisticalInfo.create.device = 1; - saveDeviceRelarion(response.id.id, deviceParameters, config).then(function success() { + statisticalInfo.create={ + device: 1 + }; + saveDeviceRelarion(response.id.id, deviceParameters, config).then(function success(response) { + angular.merge(statisticalInfo, response, statisticalInfo); deferred.resolve(statisticalInfo); }); - }, function fail(response) { - console.log(response); // eslint-disable-line + }, function fail() { if (update) { findByName(deviceParameters.name, config).then(function success(response) { - statisticalInfo.update.device = 1; - saveDeviceRelarion(response.id.id, deviceParameters, config).then(function success() { + statisticalInfo.update = { + device: 1 + }; + saveDeviceRelarion(response.id.id, deviceParameters, config).then(function success(response) { + delete Object.assign(response, {update: response.create}).create; + angular.merge(statisticalInfo, response); deferred.resolve(statisticalInfo); }); }, function fail() { @@ -384,8 +403,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom var url = '/api/tenant/devices?deviceName=' + deviceName; $http.get(url, config).then(function success(response) { deferred.resolve(response.data); - }, function fail(response) { - deferred.reject(response); + }, function fail() { + deferred.reject(); }); return deferred.promise; } diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 08d16b40c9..46bf2e9f14 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -352,6 +352,10 @@ export default angular.module('thingsboard.types', []) }, entityGroup: { columnType: { + clientAttribute: { + name: 'entity-group.column-type.client-attribute', + value: 'CLIENT_ATTRIBUTE' + }, sharedAttribute: { name: 'entity-group.column-type.shared-attribute', value: 'SHARED_ATTRIBUTE' diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js index 16670fe768..ea6ed0e7d5 100644 --- a/ui/src/app/device/device.controller.js +++ b/ui/src/app/device/device.controller.js @@ -68,7 +68,7 @@ export function DeviceController($rootScope, userService, deviceService, custome }, { onAction: function ($event) { - importExport.importDevices($event).then( + importExport.importDevices($event, types.entityType.device).then( function() { vm.grid.refreshList(); } @@ -107,7 +107,6 @@ export function DeviceController($rootScope, userService, deviceService, custome addItemTemplateUrl: addDeviceTemplate, - // addItemText: function() { return $translate.instant('device.add-device-text') }, noItemsText: function() { return $translate.instant('device.no-devices-text') }, itemDetailsText: function() { return $translate.instant('device.device-details') }, isDetailsReadOnly: isCustomerUser, @@ -338,6 +337,7 @@ export function DeviceController($rootScope, userService, deviceService, custome icon: "add" }; + vm.deviceGridConfig.addItemActions = []; } else if (vm.devicesScope === 'customer_user') { deviceActionsList.push( @@ -352,6 +352,7 @@ export function DeviceController($rootScope, userService, deviceService, custome ); vm.deviceGridConfig.addItemAction = {}; + vm.deviceGridConfig.addItemActions = []; } } diff --git a/ui/src/app/import-export/import-dialog-csv.controller.js b/ui/src/app/import-export/import-dialog-csv.controller.js index a2e4732006..18b42910be 100644 --- a/ui/src/app/import-export/import-dialog-csv.controller.js +++ b/ui/src/app/import-export/import-dialog-csv.controller.js @@ -16,7 +16,7 @@ import './import-dialog.scss'; /*@ngInject*/ -export default function ImportDialogCsvController($scope, $mdDialog, toast, importTitle, importFileLabel, importExport, types, $timeout) { +export default function ImportDialogCsvController($scope, $mdDialog, toast, importTitle, importFileLabel, entityType, importExport, types, $timeout, $q) { var vm = this; @@ -27,63 +27,80 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo vm.addDevices = addDevices; vm.importParams = { + delim: ',', isUpdate: true, isHeader: true }; + vm.selectedStep = 0; + vm.stepProgress = 1; + vm.maxStep = 3; + vm.showBusyText = false; + vm.stepData = [ + { step: 1, completed: false, optional: false, data: {} }, + { step: 2, completed: false, optional: false, data: {} }, + { step: 3, completed: false, optional: false, data: {} }, + ]; + + vm.enableNextStep = function nextStep() { + //do not exceed into max step + if (vm.selectedStep >= vm.maxStep) { + return; + } + //do not increment vm.stepProgress when submitting from previously completed step + if (vm.selectedStep === vm.stepProgress - 1) { + vm.stepProgress = vm.stepProgress + 1; + } + vm.selectedStep = vm.selectedStep + 1; + }; + + vm.moveToPreviousStep = function moveToPreviousStep() { + if (vm.selectedStep > 0) { + vm.selectedStep = vm.selectedStep - 1; + } + }; + + vm.submitCurrentStep = function submitCurrentStep(stepData, isSkip) { + var deferred = $q.defer(); + vm.showBusyText = true; + if (!stepData.completed && !isSkip) { + //simulate $http + $timeout(function () { + vm.showBusyText = false; + deferred.resolve({ status: 200, statusText: 'success', data: {} }); + //move to next step when success + stepData.completed = true; + vm.enableNextStep(); + }, 1000) + } else { + vm.showBusyText = false; + vm.enableNextStep(); + } + }; + vm.importTitle = importTitle; vm.importFileLabel = importFileLabel; + vm.entityType = entityType; vm.columnsParam = []; vm.parseData = []; - vm.entityType = types.entityType.device; - vm.columnTypes = {}; - vm.entityField = {}; + vm.delimiters = [{ + key: ',', + value: ',' + },{ + key: ';', + value: ';' + },{ + key: '|', + value: '|' + },{ + key: '\t', + value: 'Tab' + }]; var parseData = {}; - switch (vm.entityType) { - case types.entityType.device: - vm.columnTypes = types.entityGroup.columnType; - break; - } - - vm.entityField.name = types.entityGroup.entityField.name; - - switch (vm.entityType) { - case types.entityType.device: - vm.entityField.type = types.entityGroup.entityField.type; - // vm.entityField.assigned_customer = types.entityGroup.entityField.assigned_customer; - break; - } - - $scope.$watch('vm.columnsParam', function(newVal, prevVal){ - if (newVal && !angular.equals(newVal, prevVal)) { - var isSelectName = false; - var isSelectType = false; - for (var i = 0; i < newVal.length; i++) { - if (newVal[i].type === types.entityGroup.columnType.entityField.value && - newVal[i].key === types.entityGroup.entityField.name.value) { - isSelectName = true; - } - if (newVal[i].type === types.entityGroup.columnType.entityField.value && - newVal[i].key === types.entityGroup.entityField.type.value) { - isSelectType = true; - } - } - $timeout(function () { - vm.entityField.name.disable = isSelectName; - vm.entityField.type.disable = isSelectType; - }); - } - }, true); - - - function cancel() { - $mdDialog.cancel(); - } - function fileAdded($file) { if ($file.getExtension() === 'csv') { var reader = new FileReader(); @@ -94,9 +111,8 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo var importCSV = event.target.result; if (importCSV && importCSV.length > 0) { try { - vm.importData = importCSV; + parseCSV(importCSV); vm.fileName = $file.name; - parseCSVData(vm.importData); } catch (err) { vm.fileName = null; toast.showError(err.message); @@ -109,7 +125,7 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo } } - function parseCSVData(importData) { + function parseCSV(importData) { var columnParam = {}; var config = { delim: vm.importParams.delim, @@ -202,10 +218,14 @@ export default function ImportDialogCsvController($scope, $mdDialog, toast, impo function clearFile() { $scope.theForm.$setDirty(); vm.fileName = null; - vm.importData = null; + parseData = null; vm.columnsParam = []; } + function cancel() { + $mdDialog.cancel(); + } + function importFromJson() { $scope.theForm.$setPristine(); $mdDialog.hide(vm.importData); diff --git a/ui/src/app/import-export/import-dialog-csv.tpl.html b/ui/src/app/import-export/import-dialog-csv.tpl.html index a275c5c7e4..5f6cbbfcb4 100644 --- a/ui/src/app/import-export/import-dialog-csv.tpl.html +++ b/ui/src/app/import-export/import-dialog-csv.tpl.html @@ -63,66 +63,26 @@
- - - + + + + + {{delimiter.value}} + + - Use first line is header + First line is header - Update parameter device + Update parameters
- - - - - - - - - - - - - - - - - - -
 Example value dataColumn typeValue
{{$index + 1}}{{column.sampleData}} - - - {{type.name | translate}} - - - - - - {{field.name | translate}} - - - - - -
- -
+ diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index 7820532de0..dbb1ea148b 100644 --- a/ui/src/app/import-export/import-export.service.js +++ b/ui/src/app/import-export/import-export.service.js @@ -578,9 +578,9 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return deferred.promise; } - function importDevices($event) { + function importDevices($event, entityType) { var deferred = $q.defer(); - openImportDialogCSV($event, 'device.import', 'device.device-file').then( + openImportDialogCSV($event, entityType,'device.import', 'device.device-file').then( function success() { // if (!validateImportedDashboard(dashboard)) { // toast.showError($translate.instant('dashboard.invalid-dashboard-file-error')); @@ -783,10 +783,6 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return deferred.promise; } - /** - * splitCSV function (c) 2009 Brian Huisman, see http://www.greywyvern.com/?post=258 - * Works by spliting on seperators first, then patching together quoted values - */ function splitCSV(str, sep) { for (var foo = str.split(sep = sep || ","), x = foo.length - 1, tl; x >= 0; x--) { if (foo[x].replace(/"\s+$/, '"').charAt(foo[x].length - 1) == '"') { @@ -805,7 +801,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return !isNaN(parseFloat(str)) && isFinite(str); } - function parseStringToFormatJS(str) { + function convertStringToJSType(str) { if (isNumeric(str.replace(',', '.'))) { return parseFloat(str.replace(',', '.')); } @@ -827,7 +823,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, let csvlines = csvdata.split(/[\r\n]+/); let csvheaders = splitCSV(csvlines[0], delim); if (csvheaders.length < 2) { - toast.showError('A file should contain at least two columns'); + toast.showError($translate.instant('entity.import-csv-number-columns-error')); return -1; } let csvrows = header ? csvlines.slice(1, csvlines.length) : csvlines; @@ -844,34 +840,47 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, let rowitems = splitCSV(row, delim); if (rowitems.length !== result.headers.length) { - toast.showError('Invalid file format. Row:' + (header ? result.rows.length + 2: result.rows.length + 1)); + toast.showError($translate.instant('entity.import-csv-invalid-format-error', {line: (header ? result.rows.length + 2: result.rows.length + 1)})); return -1; } for (let i = 0; i < rowitems.length; i++) { - rowitems[i] = parseStringToFormatJS(rowitems[i]); + rowitems[i] = convertStringToJSType(rowitems[i]); } result.rows.push(rowitems); } } return result; } + + function sumObject(obj1, obj2){ + Object.keys(obj2).map(function(key) { + if (angular.isObject(obj2[key])) { + obj1[key] = obj1[key] || {}; + angular.merge(obj1[key], sumObject(obj1[key], obj2[key])); + } else { + obj1[key] = (obj1[key] || 0) + obj2[key]; + } + }); + return obj1; + } function createMultiEntity(arrayData, entityType, update, config) { - var deferred = $q.defer(); - var allPromise = []; + let deferred = $q.defer(); + let allPromise = []; + let statisticalInfo = {}; switch (entityType) { case types.entityType.device: - for(var i = 0; i < arrayData.length; i++){ - var promise = deviceService.saveDeviceParameters(arrayData[i], update, config); + for(let i = 0; i < arrayData.length; i++){ + const promise = deviceService.saveDeviceParameters(arrayData[i], update, config); allPromise.push(promise); } break; } - $q.all(allPromise).then(function success() { - deferred.resolve(); - $timeout(function () { - console.log("1"); // eslint-disable-line - }, 1000); + $q.all(allPromise).then(function success(response) { + for (let i = 0; i < response.length; i++){ + statisticalInfo = sumObject(statisticalInfo, response[i]); + } + deferred.resolve(statisticalInfo); }); return deferred.promise; } @@ -950,7 +959,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return deferred.promise; } - function openImportDialogCSV($event, importTitle, importFileLabel) { + function openImportDialogCSV($event, entityType, importTitle, importFileLabel) { var deferred = $q.defer(); $mdDialog.show({ controller: 'ImportDialogCSVController', @@ -958,7 +967,8 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, templateUrl: importDialogCSVTemplate, locals: { importTitle: importTitle, - importFileLabel: importFileLabel + importFileLabel: importFileLabel, + entityType: entityType }, parent: angular.element($document[0].body), multiple: true, diff --git a/ui/src/app/import-export/index.js b/ui/src/app/import-export/index.js index 54ad30e1a6..114ac98d48 100644 --- a/ui/src/app/import-export/index.js +++ b/ui/src/app/import-export/index.js @@ -16,10 +16,12 @@ import ImportExport from './import-export.service'; import ImportDialogController from './import-dialog.controller'; import ImportDialogCSVController from './import-dialog-csv.controller'; +import TableColumnsAssignment from './table-columns-assignment.directive'; export default angular.module('thingsboard.importexport', []) .factory('importExport', ImportExport) .controller('ImportDialogController', ImportDialogController) .controller('ImportDialogCSVController', ImportDialogCSVController) + .directive('tbTableColumnsAssignment', TableColumnsAssignment) .name; diff --git a/ui/src/app/import-export/table-columns-assignment.directive.js b/ui/src/app/import-export/table-columns-assignment.directive.js new file mode 100644 index 0000000000..368d4d3a90 --- /dev/null +++ b/ui/src/app/import-export/table-columns-assignment.directive.js @@ -0,0 +1,91 @@ +/* + * Copyright © 2016-2019 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 './timeinterval.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import tableColumnsAssignment from './table-columns-assignment.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function TableColumnsAssignment() { + return { + restrict: "E", + scope: true, + bindToController: { + theForm: '=?', + columns: '=', + entityType: '=', + }, + templateUrl: tableColumnsAssignment, + controller: TableColumnsAssignmentController, + controllerAs: 'vm' + }; +} + +/*@ngInject*/ +function TableColumnsAssignmentController($scope, types, $timeout) { + var vm = this; + + vm.columnTypes = {}; + vm.entityField = {}; + + switch (vm.entityType) { + case types.entityType.device: + vm.columnTypes.sharedAttribute = types.entityGroup.columnType.sharedAttribute; + vm.columnTypes.serverAttribute = types.entityGroup.columnType.serverAttribute; + vm.columnTypes.timeseries = types.entityGroup.columnType.timeseries; + vm.columnTypes.entityField = types.entityGroup.columnType.entityField; + break; + } + + vm.entityField.name = types.entityGroup.entityField.name; + + switch (vm.entityType) { + case types.entityType.device: + vm.entityField.type = types.entityGroup.entityField.type; + // vm.entityField.assigned_customer = types.entityGroup.entityField.assigned_customer; + break; + } + + $scope.$watch('vm.columns', function(newVal, prevVal){ + if (newVal && !angular.equals(newVal, prevVal)) { + var isSelectName = false; + var isSelectType = false; + for (var i = 0; i < newVal.length; i++) { + if (newVal[i].type === types.entityGroup.columnType.entityField.value && + newVal[i].key === types.entityGroup.entityField.name.value) { + isSelectName = true; + } + if (newVal[i].type === types.entityGroup.columnType.entityField.value && + newVal[i].key === types.entityGroup.entityField.type.value) { + isSelectType = true; + } + } + $timeout(function () { + vm.entityField.name.disable = isSelectName; + vm.entityField.type.disable = isSelectType; + }); + } + }, true); + + $scope.$watch('vm.columns', function(newVal, prevVal) { + if (vm.isEdit && !angular.equals(newVal, prevVal)) { + vm.theForm.$setDirty(); + } + }, true); +} diff --git a/ui/src/app/import-export/table-columns-assignment.tpl.html b/ui/src/app/import-export/table-columns-assignment.tpl.html new file mode 100644 index 0000000000..0cc0df5e6c --- /dev/null +++ b/ui/src/app/import-export/table-columns-assignment.tpl.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + +
 Example value dataColumn typeValue
{{$index + 1}}{{column.sampleData}} + + + {{type.name | translate}} + + + + + + {{field.name | translate}} + + + + + +
+ +
diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index ba82eb8e13..c9b239f8c6 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -776,13 +776,16 @@ "details": "Entity details", "no-entities-prompt": "No entities found", "no-data": "No data to display", - "columns-to-display": "Columns to Display" + "columns-to-display": "Columns to Display", + "import-csv-number-columns-error": "A file should contain at least two columns", + "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'" }, "entity-group": { "column-value": "Value", "column-title": "Title", "column-type": { "column-type": "Column type", + "client-attribute": "Client attribute", "shared-attribute": "Shared attribute", "server-attribute": "Server attribute", "timeseries": "Timeseries",