diff --git a/ui/package-lock.json b/ui/package-lock.json index 218361bd3a..bddcc6a8a1 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -8375,6 +8375,10 @@ "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", "dev": true }, + "material-steppers": { + "version": "git://github.com/thingsboard/material-steppers.git#ee6241d63b97b7c4a3f3b240cdb347c02a3dcb0a", + "from": "git://github.com/thingsboard/material-steppers.git#master" + }, "material-ui": { "version": "0.16.7", "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.16.7.tgz", diff --git a/ui/package.json b/ui/package.json index d3487e5966..8d97b2ca6d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -65,6 +65,7 @@ "leaflet": "^1.5.1", "leaflet-polylinedecorator": "^1.6.0", "leaflet-providers": "^1.8.0", + "material-steppers": "git://github.com/thingsboard/material-steppers.git#master", "material-ui": "^0.16.1", "material-ui-number-input": "^5.0.16", "md-color-picker": "0.2.6", diff --git a/ui/src/app/api/asset.service.js b/ui/src/app/api/asset.service.js index 3fddc459b3..de01620bd3 100644 --- a/ui/src/app/api/asset.service.js +++ b/ui/src/app/api/asset.service.js @@ -32,7 +32,8 @@ function AssetService($http, $q, customerService, userService) { getCustomerAssets: getCustomerAssets, findByQuery: findByQuery, fetchAssetsByNameFilter: fetchAssetsByNameFilter, - getAssetTypes: getAssetTypes + getAssetTypes: getAssetTypes, + findByName: findByName } return service; @@ -276,4 +277,16 @@ function AssetService($http, $q, customerService, userService) { return deferred.promise; } + function findByName(assetName, config) { + config = config || {}; + var deferred = $q.defer(); + var url = '/api/tenant/assets?assetName=' + assetName; + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + } diff --git a/ui/src/app/api/attribute.service.js b/ui/src/app/api/attribute.service.js index 599f44c59e..6ed2921824 100644 --- a/ui/src/app/api/attribute.service.js +++ b/ui/src/app/api/attribute.service.js @@ -30,7 +30,9 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) subscribeForEntityAttributes: subscribeForEntityAttributes, unsubscribeForEntityAttributes: unsubscribeForEntityAttributes, saveEntityAttributes: saveEntityAttributes, - deleteEntityAttributes: deleteEntityAttributes + deleteEntityAttributes: deleteEntityAttributes, + saveEntityTimeseries: saveEntityTimeseries, + deleteEntityTimeseries: deleteEntityTimeseries } return service; @@ -212,7 +214,8 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) } } - function saveEntityAttributes(entityType, entityId, attributeScope, attributes) { + function saveEntityAttributes(entityType, entityId, attributeScope, attributes, config) { + config = config || {}; var deferred = $q.defer(); var attributesData = {}; var deleteAttributes = []; @@ -229,7 +232,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) } if (Object.keys(attributesData).length) { var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope; - $http.post(url, attributesData).then(function success(response) { + $http.post(url, attributesData, config).then(function success(response) { if (deleteEntityAttributesPromise) { deleteEntityAttributesPromise.then( function success() { @@ -260,7 +263,57 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) return deferred.promise; } - function deleteEntityAttributes(entityType, entityId, attributeScope, attributes) { + function saveEntityTimeseries(entityType, entityId, timeseriesScope, timeseries, config) { + config = config || {}; + var deferred = $q.defer(); + var timeseriesData = {}; + var deleteTimeseries = []; + for (var a=0; a 0) { + keys += ','; + } + keys += timeseries[i].key; + } + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/delete' + '?keys=' + keys; + $http.delete(url, config).then(function success() { + deferred.resolve(); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } } \ No newline at end of file diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index 1a25d70eb7..429d3b0cdb 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -42,8 +42,9 @@ function DeviceService($http, $q, $window, userService, attributeService, custom sendOneWayRpcCommand: sendOneWayRpcCommand, sendTwoWayRpcCommand: sendTwoWayRpcCommand, findByQuery: findByQuery, - getDeviceTypes: getDeviceTypes - } + getDeviceTypes: getDeviceTypes, + findByName: findByName + }; return service; @@ -124,7 +125,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom if (!config) { config = {}; } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); + config = Object.assign(config, {ignoreErrors: ignoreErrors}); $http.get(url, config).then(function success(response) { deferred.resolve(response.data); }, function fail(response) { @@ -136,8 +137,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom function getDevices(deviceIds, config) { var deferred = $q.defer(); var ids = ''; - for (var i=0;i0) { + for (var i = 0; i < deviceIds.length; i++) { + if (i > 0) { ids += ','; } ids += deviceIds[i]; @@ -146,11 +147,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom $http.get(url, config).then(function success(response) { var devices = response.data; devices.sort(function (device1, device2) { - var id1 = device1.id.id; - var id2 = device2.id.id; - var index1 = deviceIds.indexOf(id1); - var index2 = deviceIds.indexOf(id2); - return index1 - index2; + var id1 = device1.id.id; + var id2 = device2.id.id; + var index1 = deviceIds.indexOf(id1); + var index2 = deviceIds.indexOf(id2); + return index1 - index2; }); deferred.resolve(devices); }, function fail(response) { @@ -159,10 +160,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom return deferred.promise; } - function saveDevice(device) { + function saveDevice(device, config) { + config = config || {}; var deferred = $q.defer(); var url = '/api/device'; - $http.post(url, device).then(function success(response) { + $http.post(url, device, config).then(function success(response) { deferred.resolve(response.data); }, function fail() { deferred.reject(); @@ -181,7 +183,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom return deferred.promise; } - function getDeviceCredentials(deviceId, sync) { + function getDeviceCredentials(deviceId, sync, config) { + config = config || {}; var deferred = $q.defer(); var url = '/api/device/' + deviceId + '/credentials'; if (sync) { @@ -196,7 +199,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom deferred.reject(); } } else { - $http.get(url, null).then(function success(response) { + $http.get(url, config).then(function success(response) { deferred.resolve(response.data); }, function fail() { deferred.reject(); @@ -205,10 +208,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom return deferred.promise; } - function saveDeviceCredentials(deviceCredentials) { + function saveDeviceCredentials(deviceCredentials, config) { + config = config || {}; var deferred = $q.defer(); var url = '/api/device/credentials'; - $http.post(url, deviceCredentials).then(function success(response) { + $http.post(url, deviceCredentials, config).then(function success(response) { deferred.resolve(response.data); }, function fail() { deferred.reject(); @@ -297,7 +301,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom if (!config) { config = {}; } - config = Object.assign(config, { ignoreErrors: ignoreErrors }); + config = Object.assign(config, {ignoreErrors: ignoreErrors}); $http.post(url, query, config).then(function success(response) { deferred.resolve(response.data); }, function fail() { @@ -317,4 +321,15 @@ function DeviceService($http, $q, $window, userService, attributeService, custom return deferred.promise; } + function findByName(deviceName, config) { + config = config || {}; + var deferred = $q.defer(); + var url = '/api/tenant/devices?deviceName=' + deviceName; + $http.get(url, config).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } } diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index 42b57596e5..6cadc1ca25 100644 --- a/ui/src/app/api/entity.service.js +++ b/ui/src/app/api/entity.service.js @@ -38,6 +38,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device createAlarmSourceFromSubscriptionInfo: createAlarmSourceFromSubscriptionInfo, getRelatedEntities: getRelatedEntities, saveRelatedEntity: saveRelatedEntity, + saveEntityParameters: saveEntityParameters, getRelatedEntity: getRelatedEntity, deleteRelatedEntity: deleteRelatedEntity, moveEntity: moveEntity, @@ -1072,6 +1073,119 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return deferred.promise; } + function saveEntityRelation(entityType, entityId, entityRelation, config) { + const deferred = $q.defer(); + let attributesType = Object.keys(types.attributesScope); + let allPromise = []; + let promise = ""; + if (entityRelation.accessToken !== "") { + promise = deviceService.getDeviceCredentials(entityId.id, null, config).then(function (response) { + response.credentialsId = entityRelation.accessToken; + response.credentialsType = "ACCESS_TOKEN"; + response.credentialsValue = null; + return deviceService.saveDeviceCredentials(response, config).catch(function () { + return "error"; + }); + }); + allPromise.push(promise) + } + for (let i = 0; i < attributesType.length; i++) { + let attribute = attributesType[i]; + if (entityRelation.attributes[attribute] && entityRelation.attributes[attribute].length !== 0) { + promise = attributeService.saveEntityAttributes(entityType, entityId.id, types.attributesScope[attribute].value, entityRelation.attributes[attribute], config).catch(function () { + return "error"; + }); + allPromise.push(promise); + } + } + if (entityRelation.timeseries.length !== 0) { + promise = attributeService.saveEntityTimeseries(entityType, entityId.id, "time", entityRelation.timeseries, config).catch(function(){ + return "error"; + }); + allPromise.push(promise); + } + $q.all(allPromise).then(function success(response) { + let isResponseHasError = false; + for(let i = 0; i < response.length; i++){ + if(response[i] === "error"){ + isResponseHasError = true; + break; + } + } + isResponseHasError ? deferred.reject() : deferred.resolve(); + }); + return deferred.promise; + } + + function saveEntityParameters(entityType, entityParameters, update, config) { + config = config || {}; + const deferred = $q.defer(); + let statisticalInfo = {}; + let newEntity = { + name: entityParameters.name, + type: entityParameters.type + }; + let promise; + switch (entityType) { + case types.entityType.device: + promise = deviceService.saveDevice(newEntity, config); + break; + case types.entityType.asset: + promise = assetService.saveAsset(newEntity, true, config); + break; + } + + promise.then(function success(response) { + saveEntityRelation(entityType, response.id, entityParameters, config).then(function success() { + statisticalInfo.create = { + entity: 1 + }; + deferred.resolve(statisticalInfo); + }, function fail() { + statisticalInfo.error = { + entity: 1 + }; + deferred.resolve(statisticalInfo); + }); + }, function fail() { + if (update) { + let findIdEntity; + switch (entityType) { + case types.entityType.device: + findIdEntity = deviceService.findByName(entityParameters.name, config); + break; + case types.entityType.asset: + findIdEntity = assetService.findByName(entityParameters.name, config); + break; + } + findIdEntity.then(function success(response) { + saveEntityRelation(entityType, response.id, entityParameters, config).then(function success() { + statisticalInfo.update = { + entity: 1 + }; + deferred.resolve(statisticalInfo); + }, function fail() { + statisticalInfo.error = { + entity: 1 + }; + deferred.resolve(statisticalInfo); + }); + }, function fail() { + statisticalInfo.error = { + entity: 1 + }; + deferred.resolve(statisticalInfo); + }); + } else { + statisticalInfo.error = { + entity: 1 + }; + deferred.resolve(statisticalInfo); + } + }); + return deferred.promise; + } + function getRelatedEntity(entityId, keys, typeTranslatePrefix) { var deferred = $q.defer(); getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then( diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 3fd4115a2b..27bf94a19f 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -54,6 +54,8 @@ import react from 'ngreact'; import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; import 'ngFlowchart/dist/ngFlowchart'; import 'jstree/dist/jstree.min'; +import 'material-steppers/dist/material-steppers'; +import 'material-steppers/dist/material-steppers.css' import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css'; import 'typeface-roboto'; import 'font-awesome/css/font-awesome.min.css'; @@ -127,6 +129,7 @@ angular.module('thingsboard', [ react.name, 'flow', 'flowchart', + 'mdSteppers', thingsboardThirdpartyFix, thingsboardTranslateHandler, thingsboardLogin, diff --git a/ui/src/app/asset/asset.controller.js b/ui/src/app/asset/asset.controller.js index 9dc0ce3505..27d68f0878 100644 --- a/ui/src/app/asset/asset.controller.js +++ b/ui/src/app/asset/asset.controller.js @@ -48,7 +48,7 @@ export function AssetCardController(types) { /*@ngInject*/ export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams, - $document, $mdDialog, $q, $translate, types) { + $document, $mdDialog, $q, $translate, types, importExport) { var customerId = $stateParams.customerId; @@ -56,6 +56,29 @@ export function AssetController($rootScope, userService, assetService, customerS var assetGroupActionsList = []; + var assetAddItemActionsList = [ + { + onAction: function ($event) { + vm.grid.addItem($event); + }, + name: function() { return $translate.instant('action.add') }, + details: function() { return $translate.instant('asset.add-asset-text') }, + icon: "insert_drive_file" + }, + { + onAction: function ($event) { + importExport.importEntities($event, types.entityType.asset).then( + function() { + vm.grid.refreshList(); + } + ); + }, + name: function() { return $translate.instant('action.import') }, + details: function() { return $translate.instant('asset.import') }, + icon: "file_upload" + } + ]; + var vm = this; vm.types = types; @@ -77,6 +100,7 @@ export function AssetController($rootScope, userService, assetService, customerS actionsList: assetActionsList, groupActionsList: assetGroupActionsList, + addItemActions: assetAddItemActionsList, onGridInited: gridInited, @@ -294,6 +318,8 @@ export function AssetController($rootScope, userService, assetService, customerS } else if (vm.assetsScope === 'customer_user') { vm.assetGridConfig.addItemAction = {}; } + vm.assetGridConfig.addItemActions = []; + } vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction; diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 8e85e1a054..8d5468fd8d 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -350,6 +350,40 @@ export default angular.module('thingsboard.types', []) rulenode: "RULE_NODE", entityView: "ENTITY_VIEW" }, + importEntityColumnType: { + name: { + name: 'import.column-type.name', + value: 'name' + }, + type: { + name: 'import.column-type.type', + value: 'type' + }, + clientAttribute: { + name: 'import.column-type.client-attribute', + value: 'CLIENT_ATTRIBUTE' + }, + sharedAttribute: { + name: 'import.column-type.shared-attribute', + value: 'SHARED_ATTRIBUTE' + }, + serverAttribute: { + name: 'import.column-type.server-attribute', + value: 'SERVER_ATTRIBUTE' + }, + timeseries: { + name: 'import.column-type.timeseries', + value: 'TIMESERIES' + }, + entityField: { + name: 'import.column-type.entity-field', + value: 'ENTITY_FIELD' + }, + accessToken: { + name: 'import.column-type.access-token', + value: 'ACCESS_TOKEN' + } + }, aliasEntityType: { current_customer: "CURRENT_CUSTOMER" }, diff --git a/ui/src/app/device/device.controller.js b/ui/src/app/device/device.controller.js index ec10b57632..e015ef098d 100644 --- a/ui/src/app/device/device.controller.js +++ b/ui/src/app/device/device.controller.js @@ -49,7 +49,7 @@ export function DeviceCardController(types) { /*@ngInject*/ export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams, - $document, $mdDialog, $q, $translate, types) { + $document, $mdDialog, $q, $translate, types, importExport) { var customerId = $stateParams.customerId; @@ -57,6 +57,29 @@ export function DeviceController($rootScope, userService, deviceService, custome var deviceGroupActionsList = []; + var deviceAddItemActionsList = [ + { + onAction: function ($event) { + vm.grid.addItem($event); + }, + name: function() { return $translate.instant('action.add') }, + details: function() { return $translate.instant('device.add-device-text') }, + icon: "insert_drive_file" + }, + { + onAction: function ($event) { + importExport.importEntities($event, types.entityType.device).then( + function() { + vm.grid.refreshList(); + } + ); + }, + name: function() { return $translate.instant('action.import') }, + details: function() { return $translate.instant('device.import') }, + icon: "file_upload" + } + ]; + var vm = this; vm.types = types; @@ -78,12 +101,12 @@ export function DeviceController($rootScope, userService, deviceService, custome actionsList: deviceActionsList, groupActionsList: deviceGroupActionsList, + addItemActions: deviceAddItemActionsList, onGridInited: gridInited, 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, @@ -314,7 +337,6 @@ export function DeviceController($rootScope, userService, deviceService, custome icon: "add" }; - } else if (vm.devicesScope === 'customer_user') { deviceActionsList.push( { @@ -329,6 +351,8 @@ export function DeviceController($rootScope, userService, deviceService, custome vm.deviceGridConfig.addItemAction = {}; } + vm.deviceGridConfig.addItemActions = []; + } vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction; diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js index f45bf89d81..2d4eb43132 100644 --- a/ui/src/app/global-interceptor.service.js +++ b/ui/src/app/global-interceptor.service.js @@ -21,6 +21,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { var userService; var types; var http; + var timeout; var internalUrlPrefixes = [ '/api/auth/token', @@ -71,6 +72,13 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { return http; } + function getTimeout() { + if (!timeout) { + timeout = $injector.get("$timeout"); + } + return timeout; + } + function rejectionErrorCode(rejection) { if (rejection && rejection.data && rejection.data.errorCode) { return rejection.data.errorCode; @@ -144,12 +152,20 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { return response; } + function retryRequest (httpConfig) { + var thisTimeout = 1000 + Math.random() * 3000; + return getTimeout()(function() { + return getHttp()(httpConfig); + }, thisTimeout); + } + function responseError(rejection) { if (rejection.config.url.startsWith('/api/')) { updateLoadingState(rejection.config, false); } var unhandled = false; var ignoreErrors = rejection.config.ignoreErrors; + var resendRequest = rejection.config.resendRequest; if (rejection.refreshTokenPending || rejection.status === 401) { var errorCode = rejectionErrorCode(rejection); if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { @@ -161,6 +177,10 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { if (!ignoreErrors) { $rootScope.$broadcast('forbidden'); } + } else if (rejection.status === 429) { + if (resendRequest) { + return retryRequest(rejection.config); + } } else if (rejection.status === 0 || rejection.status === -1) { getToast().showError(getTranslate().instant('error.unable-to-connect')); } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) { diff --git a/ui/src/app/help/help-links.constant.js b/ui/src/app/help/help-links.constant.js index 7f49496550..dbe96377e6 100644 --- a/ui/src/app/help/help-links.constant.js +++ b/ui/src/app/help/help-links.constant.js @@ -96,6 +96,7 @@ export default angular.module('thingsboard.help', []) assets: helpBaseUrl + "/docs/user-guide/ui/assets", devices: helpBaseUrl + "/docs/user-guide/ui/devices", entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views", + entitiesImport: helpBaseUrl + "/docs/user-guide/bulk-provisioning", dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards", users: helpBaseUrl + "/docs/user-guide/ui/users", widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles", diff --git a/ui/src/app/import-export/import-dialog-csv.controller.js b/ui/src/app/import-export/import-dialog-csv.controller.js new file mode 100644 index 0000000000..a7b5329205 --- /dev/null +++ b/ui/src/app/import-export/import-dialog-csv.controller.js @@ -0,0 +1,238 @@ +/* + * 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 './import-dialog.scss'; + +/*@ngInject*/ +export default function ImportDialogCsvController($scope, $mdDialog, toast, importTitle, importFileLabel, entityType, importExport, types, $mdStepper, $timeout) { + + var vm = this; + + vm.cancel = cancel; + vm.fileAdded = fileAdded; + vm.clearFile = clearFile; + vm.nextStep = nextStep; + vm.previousStep = previousStep; + + vm.importParameters = { + delim: ',', + isUpdate: true, + isHeader: true + }; + + vm.importTitle = importTitle; + vm.importFileLabel = importFileLabel; + vm.entityType = entityType; + + vm.isVertical = true; + vm.isLinear = true; + vm.isAlternative = false; + vm.isMobileStepText = true; + vm.isImportData = false; + + vm.parseData = []; + + vm.delimiters = [{ + key: ',', + value: ',' + }, { + key: ';', + value: ';' + }, { + key: '|', + value: '|' + }, { + key: '\t', + value: 'Tab' + }]; + + vm.progressCreate = 0; + + var parseData = {}; + + function fileAdded($file) { + if ($file.getExtension() === 'csv') { + var reader = new FileReader(); + reader.onload = function (event) { + $scope.$apply(function () { + if (event.target.result) { + vm.theFormStep1.$setDirty(); + var importCSV = event.target.result; + if (importCSV && importCSV.length > 0) { + try { + vm.importData = importCSV; + vm.fileName = $file.name; + } catch (err) { + vm.fileName = null; + toast.showError(err.message); + } + } + } + }); + }; + reader.readAsText($file.file); + } + } + + function parseCSV(importData) { + var config = { + delim: vm.importParameters.delim, + header: vm.importParameters.isHeader + }; + return importExport.convertCSVToJson(importData, config); + } + + function createColumnsData(parseData) { + vm.columnsParam = []; + var columnParam = {}; + for (var i = 0; i < parseData.headers.length; i++) { + if (vm.importParameters.isHeader && parseData.headers[i].search(/^(name|type)$/im) === 0) { + columnParam = { + type: types.importEntityColumnType[parseData.headers[i].toLowerCase()].value, + key: parseData.headers[i].toLowerCase(), + sampleData: parseData.rows[0][i] + }; + } else { + columnParam = { + type: types.importEntityColumnType.serverAttribute.value, + key: vm.importParameters.isHeader ? parseData.headers[i] : "", + sampleData: parseData.rows[0][i] + }; + } + vm.columnsParam.push(columnParam); + } + } + + function addEntities (importData, parameterColumns) { + var entitiesData = []; + var sentDataLength = 0; + var config = { + ignoreErrors: true, + resendRequest: true + }; + for (var i = 0; i < importData.rows.length; i++) { + var entityData = { + name: "", + type: "", + accessToken: "", + attributes: { + server: [], + shared: [] + }, + timeseries: [] + }; + for (var j = 0; j < parameterColumns.length; j++) { + switch (parameterColumns[j].type) { + case types.importEntityColumnType.serverAttribute.value: + entityData.attributes.server.push({ + key: parameterColumns[j].key, + value: importData.rows[i][j] + }); + break; + case types.importEntityColumnType.timeseries.value: + entityData.timeseries.push({ + key: parameterColumns[j].key, + value: importData.rows[i][j] + }); + break; + case types.importEntityColumnType.sharedAttribute.value: + entityData.attributes.shared.push({ + key: parameterColumns[j].key, + value: importData.rows[i][j] + }); + break; + case types.importEntityColumnType.accessToken.value: + entityData.accessToken = importData.rows[i][j]; + break; + case types.importEntityColumnType.name.value: + entityData.name = importData.rows[i][j]; + break; + case types.importEntityColumnType.type.value: + entityData.type = importData.rows[i][j]; + break; + } + } + entitiesData.push(entityData); + } + $scope.$on('createImportEntityCompleted', function () { + sentDataLength++; + vm.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100); + }); + importExport.createMultiEntity(entitiesData, vm.entityType, vm.importParameters.isUpdate, config).then(function (response) { + vm.statistical = response; + vm.isImportData = false; + $mdStepper('import-stepper').next(); + }); + } + + function clearFile() { + vm.theFormStep1.$setDirty(); + vm.fileName = null; + vm.importData = null; + } + + function previousStep(step) { + let steppers = $mdStepper('import-stepper'); + switch (step) { + case 1: + steppers.back(); + $timeout(function () { + vm.theFormStep1.$setDirty(); + }); + break; + default: + steppers.back(); + break; + } + } + + function nextStep(step) { + let steppers = $mdStepper('import-stepper'); + switch (step) { + case 2: + steppers.next(); + break; + case 3: + parseData = parseCSV(vm.importData); + if (parseData === -1) { + steppers.back(); + $timeout(function () { + clearFile(); + }); + } else { + createColumnsData(parseData); + steppers.next(); + } + break; + case 4: + steppers.next(); + vm.isImportData = true; + addEntities(parseData, vm.columnsParam); + break; + case 6: + $mdDialog.hide(); + break; + } + + } + + function cancel() { + if($mdStepper('import-stepper').currentStep > 2){ + $mdDialog.hide(); + } else { + $mdDialog.cancel(); + } + } +} diff --git a/ui/src/app/import-export/import-dialog-csv.tpl.html b/ui/src/app/import-export/import-dialog-csv.tpl.html new file mode 100644 index 0000000000..50c8f83d7b --- /dev/null +++ b/ui/src/app/import-export/import-dialog-csv.tpl.html @@ -0,0 +1,171 @@ + + + +
+

{{ vm.importTitle }}

+ +
+ + + +
+
+ + + + + + +
+
+
+
+ +
+
+ + + {{ 'action.remove' | translate }} + + + close + + +
+
+ + +
+
+
+
+
import.no-file
+
{{ vm.fileName }}
+
+
+
+
+
+ + + + + {{ 'action.cancel' | translate }} + + + {{ 'action.continue' | translate }} + + +
+ + + +
+ + + + + {{delimiter.value}} + + + + + {{ 'import.csv-first-line-header' | translate }} + + + {{ 'import.csv-update-data' | translate }} + +
+
+ + + Back + + + + {{ 'action.cancel' | translate }} + + + {{ 'action.continue' | translate }} + + +
+ + + +
+ +
+
+ + + Back + + + + {{ 'action.cancel' | translate }} + + + {{ 'action.continue' | translate }} + + +
+ + + + + + + + +
+

import.message.create-entities

+

import.message.update-entities

+

import.message.error-entities

+
+
+ + + + {{ 'action.ok' | translate }} + + +
+ +
+
+
diff --git a/ui/src/app/import-export/import-dialog.scss b/ui/src/app/import-export/import-dialog.scss index 44e2802f31..c658763974 100644 --- a/ui/src/app/import-export/import-dialog.scss +++ b/ui/src/app/import-export/import-dialog.scss @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@import "../../scss/constants.scss"; + $previewSize: 100px !default; .tb-file-select-container { @@ -40,3 +42,21 @@ $previewSize: 100px !default; top: 50%; transform: translate(0%, -50%) !important; } + +.tb-table-select{ + md-input-container{ + margin: 0; + + .md-errors-spacer{ + min-height: 0; + } + } +} + +.md-stepper-indicator-wrapper{ + background-color: transparent; +} + +.tb-import-progress{ + margin: 7px 0; +} diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js index 0bbf6f5523..f86c79c148 100644 --- a/ui/src/app/import-export/import-export.service.js +++ b/ui/src/app/import-export/import-export.service.js @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable import/no-unresolved, import/default */ +/* eslint-disable import/no-unresolved, import/default*/ import importDialogTemplate from './import-dialog.tpl.html'; +import importDialogCSVTemplate from './import-dialog-csv.tpl.html'; import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ @@ -24,7 +25,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; /* eslint-disable no-undef, angular/window-service, angular/document-service */ /*@ngInject*/ -export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, +export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, $rootScope, dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) { @@ -41,7 +42,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, importWidgetsBundle: importWidgetsBundle, exportExtension: exportExtension, importExtension: importExtension, - exportToPc: exportToPc + importEntities: importEntities, + convertCSVToJson: convertCSVToJson, + exportToPc: exportToPc, + createMultiEntity: createMultiEntity }; return service; @@ -54,11 +58,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, var bundleAlias = widgetsBundle.alias; var isSystem = widgetsBundle.tenantId.id === types.id.nullUid; widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( - function success (widgetTypes) { + function success(widgetTypes) { prepareExport(widgetsBundle); var widgetsBundleItem = { - widgetsBundle: prepareExport(widgetsBundle), - widgetTypes: [] + widgetsBundle: prepareExport(widgetsBundle), + widgetTypes: [] }; for (var t in widgetTypes) { var widgetType = widgetTypes[t]; @@ -68,10 +72,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, widgetsBundleItem.widgetTypes.push(prepareExport(widgetType)); } var name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g,"_"); + name = name.toLowerCase().replace(/\W/g, "_"); exportToPc(widgetsBundleItem, name + '.json'); }, - function fail (rejection) { + function fail(rejection) { var message = rejection; if (!message) { message = $translate.instant('error.unknown-error'); @@ -168,7 +172,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, delete widgetType.bundleAlias; } var name = widgetType.name; - name = name.toLowerCase().replace(/\W/g,"_"); + name = name.toLowerCase().replace(/\W/g, "_"); exportToPc(prepareExport(widgetType), name + '.json'); }, function fail(rejection) { @@ -209,8 +213,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, function validateImportedWidgetType(widgetType) { if (angular.isUndefined(widgetType.name) - || angular.isUndefined(widgetType.descriptor)) - { + || angular.isUndefined(widgetType.descriptor)) { return false; } return true; @@ -228,7 +231,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, metadata: prepareRuleChainMetaData(ruleChainMetaData) }; var name = ruleChain.name; - name = name.toLowerCase().replace(/\W/g,"_"); + name = name.toLowerCase().replace(/\W/g, "_"); exportToPc(ruleChainExport, name + '.json'); }, (rejection) => { @@ -253,7 +256,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, function prepareRuleChainMetaData(ruleChainMetaData) { delete ruleChainMetaData.ruleChainId; - for (var i=0;i 0) { processEntityAliases(entityAliases, aliasIds).then( - function(missingEntityAliases) { + function (missingEntityAliases) { if (Object.keys(missingEntityAliases).length > 0) { - editMissingAliases($event, [ widget ], - true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then( + editMissingAliases($event, [widget], + true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then( function success(updatedEntityAliases) { for (var aliasId in updatedEntityAliases) { var entityAlias = updatedEntityAliases[aliasId]; @@ -483,14 +486,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, function success(targetLayout) { itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget, aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then( - function() { - deferred.resolve( - { - widget: widget, - layoutId: targetLayout - } - ); - } + function () { + deferred.resolve( + { + widget: widget, + layoutId: targetLayout + } + ); + } ); }, function fail() { @@ -505,7 +508,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, dashboardService.getDashboard(dashboardId).then( function success(dashboard) { var name = dashboard.title; - name = name.toLowerCase().replace(/\W/g,"_"); + name = name.toLowerCase().replace(/\W/g, "_"); exportToPc(prepareDashboardExport(dashboard), name + '.json'); }, function fail(rejection) { @@ -538,13 +541,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); var entityAliases = dashboard.configuration.entityAliases; if (entityAliases) { - var aliasIds = Object.keys( entityAliases ); + var aliasIds = Object.keys(entityAliases); if (aliasIds.length > 0) { processEntityAliases(entityAliases, aliasIds).then( - function(missingEntityAliases) { - if (Object.keys( missingEntityAliases ).length > 0) { + function (missingEntityAliases) { + if (Object.keys(missingEntityAliases).length > 0) { editMissingAliases($event, dashboard.configuration.widgets, - false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then( + false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then( function success(updatedEntityAliases) { for (var aliasId in updatedEntityAliases) { entityAliases[aliasId] = updatedEntityAliases[aliasId]; @@ -575,6 +578,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return deferred.promise; } + function importEntities($event, entityType) { + var deferred = $q.defer(); + + switch (entityType) { + case types.entityType.device: + openImportDialogCSV($event, entityType, 'device.import', 'device.device-file').then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + case types.entityType.asset: + openImportDialogCSV($event, entityType, 'asset.import', 'asset.asset-file').then( + function success() { + deferred.resolve(); + }, + function fail() { + deferred.reject(); + } + ); + return deferred.promise; + } + + } + function saveImportedDashboard(dashboard, deferred) { dashboardService.saveDashboard(dashboard).then( function success() { @@ -594,14 +625,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, } - function exportExtension(extensionId) { getExtension(extensionId) .then( function success(extension) { var name = extension.title; - name = name.toLowerCase().replace(/\W/g,"_"); + name = name.toLowerCase().replace(/\W/g, "_"); exportToPc(prepareExport(extension), name + '.json'); }, function fail(rejection) { @@ -661,7 +691,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, function validateImportedExtension(configuration) { if (configuration.length) { for (let i = 0; i < configuration.length; i++) { - if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) { + if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id) || angular.isUndefined(configuration[i].type)) { return false; } } @@ -692,7 +722,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, var aliasId = aliasIds[index]; var entityAlias = entityAliases[aliasId]; entityService.checkEntityAlias(entityAlias).then( - function(result) { + function (result) { if (result) { checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); } else { @@ -732,6 +762,124 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return deferred.promise; } + 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) == '"') { + if ((tl = foo[x].replace(/^\s+"/, '"')).length > 1 && tl.charAt(0) == '"') { + foo[x] = foo[x].replace(/^\s*"|"\s*$/g, '').replace(/""/g, '"'); + } else if (x) { + foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep)); + } else foo = foo.shift().split(sep).concat(foo); + } else foo[x].replace(/""/g, '"'); + } + return foo; + } + + function isNumeric(str) { + str = str.replace(',', '.'); + return !isNaN(parseFloat(str)) && isFinite(str); + } + + function convertStringToJSType(str) { + if (isNumeric(str.replace(',', '.'))) { + return parseFloat(str.replace(',', '.')); + } + if (str.search(/^(true|false)$/im) === 0) { + return str === 'true'; + } + if (str === "") { + return null; + } + return str; + } + + function convertCSVToJson(csvdata, config) { + config = config || {}; + const delim = config.delim || ","; + const header = config.header || false; + let result = {}; + + let csvlines = csvdata.split(/[\r\n]+/); + let csvheaders = splitCSV(csvlines[0], delim); + if (csvheaders.length < 2) { + toast.showError($translate.instant('import.import-csv-number-columns-error')); + return -1; + } + let csvrows = header ? csvlines.slice(1, csvlines.length) : csvlines; + + result.headers = csvheaders; + result.rows = []; + + for (let r in csvrows) { + if (csvrows.hasOwnProperty(r)) { + let row = csvrows[r]; + + if (row.length === 0) + break; + + let rowitems = splitCSV(row, delim); + if (rowitems.length !== result.headers.length) { + toast.showError($translate.instant('import.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] = 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 qAllWithProgress(promises) { + promises.forEach(function(p) { + p.then(function() { + $rootScope.$broadcast('createImportEntityCompleted', {}); + }); + }); + return $q.all(promises); + } + + function createMultiEntity(arrayData, entityType, updateData, config) { + let partSize = 100; + partSize = arrayData.length > partSize ? partSize : arrayData.length; + let allPromise = []; + let statisticalInfo = {}; + let deferred = $q.defer(); + + for(let i = 0; i < partSize; i++){ + const promise = entityService.saveEntityParameters(entityType, arrayData[i], updateData, config); + allPromise.push(promise); + } + + qAllWithProgress(allPromise).then(function success(response) { + for (let i = 0; i < response.length; i++){ + statisticalInfo = sumObject(statisticalInfo, response[i]); + } + arrayData.splice(0, partSize); + if(arrayData.length > 0){ + deferred.resolve(createMultiEntity(arrayData, entityType, updateData, config).then(function (response) { + return sumObject(statisticalInfo, response); + })); + } else { + deferred.resolve(statisticalInfo); + } + }); + return deferred.promise; + } + // Common functions function prepareExport(data) { @@ -771,8 +919,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, if (window.navigator && window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveOrOpenBlob(blob, filename); - } - else{ + } else { var e = document.createEvent('MouseEvents'), a = document.createElement('a'); @@ -807,6 +954,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, return deferred.promise; } + function openImportDialogCSV($event, entityType, importTitle, importFileLabel) { + var deferred = $q.defer(); + $mdDialog.show({ + controller: 'ImportDialogCSVController', + controllerAs: 'vm', + templateUrl: importDialogCSVTemplate, + locals: { + importTitle: importTitle, + importFileLabel: importFileLabel, + entityType: entityType + }, + parent: angular.element($document[0].body), + onComplete: fixedDialogSize, + multiple: true, + fullscreen: true, + targetEvent: $event + }).then(function () { + deferred.resolve(); + }, function () { + deferred.reject(); + }); + return deferred.promise; + } + + function fixedDialogSize(scope, element) { + let dialogElement = element[0].getElementsByTagName('md-dialog'); + dialogElement[0].style.width = dialogElement[0].offsetWidth + 2 + "px"; + } } /* eslint-enable no-undef, angular/window-service, angular/document-service */ diff --git a/ui/src/app/import-export/index.js b/ui/src/app/import-export/index.js index 6ee8abc808..114ac98d48 100644 --- a/ui/src/app/import-export/index.js +++ b/ui/src/app/import-export/index.js @@ -15,9 +15,13 @@ */ 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..a645e07392 --- /dev/null +++ b/ui/src/app/import-export/table-columns-assignment.directive.js @@ -0,0 +1,93 @@ +/* + * 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. + */ + +/* 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.columnTypes.name = types.importEntityColumnType.name; + vm.columnTypes.type = types.importEntityColumnType.type; + + switch (vm.entityType) { + case types.entityType.device: + vm.columnTypes.sharedAttribute = types.importEntityColumnType.sharedAttribute; + vm.columnTypes.serverAttribute = types.importEntityColumnType.serverAttribute; + vm.columnTypes.timeseries = types.importEntityColumnType.timeseries; + vm.columnTypes.accessToken = types.importEntityColumnType.accessToken; + break; + case types.entityType.asset: + vm.columnTypes.serverAttribute = types.importEntityColumnType.serverAttribute; + vm.columnTypes.timeseries = types.importEntityColumnType.timeseries; + break; + } + + $scope.$watch('vm.columns', function(newVal){ + if (newVal) { + var isSelectName = false; + var isSelectType = false; + var isSelectCredentials = false; + for (var i = 0; i < newVal.length; i++) { + switch (newVal[i].type) { + case types.importEntityColumnType.name.value: + isSelectName = true; + break; + case types.importEntityColumnType.type.value: + isSelectType = true; + break; + case types.importEntityColumnType.accessToken.value: + isSelectCredentials = true; + break; + } + } + if(isSelectName && isSelectType) { + vm.theForm.$setDirty(); + } else { + vm.theForm.$setPristine(); + } + $timeout(function () { + vm.columnTypes.name.disable = isSelectName; + vm.columnTypes.type.disable = isSelectType; + if (angular.isDefined(vm.columnTypes.accessToken)) { + vm.columnTypes.accessToken.disable = isSelectCredentials; + } + }); + } + }, 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..f00a08251c --- /dev/null +++ b/ui/src/app/import-export/table-columns-assignment.tpl.html @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + +
 {{ 'import.column-example' | translate }}{{ 'import.column-type.column-type' | translate }}{{ 'import.column-key' | translate }}
{{$index + 1}}{{column.sampleData}} + + + {{type.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 567b3d3eae..e6a0a956cc 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -48,7 +48,8 @@ "paste-reference": "Paste reference", "import": "Import", "export": "Export", - "share-via": "Share via {{provider}}" + "share-via": "Share via {{provider}}", + "continue": "Continue" }, "aggregation": { "aggregation": "Aggregation", @@ -246,7 +247,9 @@ "select-asset": "Select asset", "no-assets-matching": "No assets matching '{{entity}}' were found.", "asset-required": "Asset is required", - "name-starts-with": "Asset name starts with" + "name-starts-with": "Asset name starts with", + "import": "Import assets", + "asset-file": "Asset file" }, "attribute": { "attributes": "Attributes", @@ -665,7 +668,9 @@ "is-gateway": "Is gateway", "public": "Public", "device-public": "Device is public", - "select-device": "Select device" + "select-device": "Select device", + "import": "Import device", + "device-file": "Device file" }, "dialog": { "close": "Close dialog" @@ -1093,7 +1098,40 @@ }, "import": { "no-file": "No file selected", - "drop-file": "Drop a JSON file or click to select a file to upload." + "drop-file": "Drop a JSON file or click to select a file to upload.", + "drop-file-csv": "Drop a CSV file or click to select a file to upload.", + "column-value": "Value", + "column-title": "Title", + "column-example": "Example value data", + "column-key": "Attribute/telemetry key", + "csv-delimiter": "CSV delimiter", + "csv-first-line-header": "First line contains column names", + "csv-update-data": "Update attributes/telemetry", + "import-csv-number-columns-error": "A file should contain at least two columns", + "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'", + "column-type": { + "name": "Name", + "type": "Type", + "column-type": "Column type", + "client-attribute": "Client attribute", + "shared-attribute": "Shared attribute", + "server-attribute": "Server attribute", + "timeseries": "Timeseries", + "entity-field": "Entity field", + "access-token": "Access token" + }, + "stepper-text":{ + "select-file": "Select a file", + "configuration": "Import configuration", + "column-type": "Select columns type", + "creat-entities": "Creating new entities", + "done": "Done" + }, + "message": { + "create-entities": "{{count}} new entities were successfully created.", + "update-entities": "{{count}} entities were successfully updated.", + "error-entities": "There was an error creating {{count}} entities." + } }, "item": { "selected": "Selected"