Commit eaddbbaf23f8d0c62372cd4d70115670b7a27215

Authored by Igor Kulikov
2 parents 4b1bfe93 3dd78ea4

Fix conflicts

@@ -8375,6 +8375,10 @@ @@ -8375,6 +8375,10 @@
8375 "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", 8375 "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=",
8376 "dev": true 8376 "dev": true
8377 }, 8377 },
  8378 + "material-steppers": {
  8379 + "version": "git://github.com/thingsboard/material-steppers.git#ee6241d63b97b7c4a3f3b240cdb347c02a3dcb0a",
  8380 + "from": "git://github.com/thingsboard/material-steppers.git#master"
  8381 + },
8378 "material-ui": { 8382 "material-ui": {
8379 "version": "0.16.7", 8383 "version": "0.16.7",
8380 "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.16.7.tgz", 8384 "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.16.7.tgz",
@@ -65,6 +65,7 @@ @@ -65,6 +65,7 @@
65 "leaflet": "^1.5.1", 65 "leaflet": "^1.5.1",
66 "leaflet-polylinedecorator": "^1.6.0", 66 "leaflet-polylinedecorator": "^1.6.0",
67 "leaflet-providers": "^1.8.0", 67 "leaflet-providers": "^1.8.0",
  68 + "material-steppers": "git://github.com/thingsboard/material-steppers.git#master",
68 "material-ui": "^0.16.1", 69 "material-ui": "^0.16.1",
69 "material-ui-number-input": "^5.0.16", 70 "material-ui-number-input": "^5.0.16",
70 "md-color-picker": "0.2.6", 71 "md-color-picker": "0.2.6",
@@ -32,7 +32,8 @@ function AssetService($http, $q, customerService, userService) { @@ -32,7 +32,8 @@ function AssetService($http, $q, customerService, userService) {
32 getCustomerAssets: getCustomerAssets, 32 getCustomerAssets: getCustomerAssets,
33 findByQuery: findByQuery, 33 findByQuery: findByQuery,
34 fetchAssetsByNameFilter: fetchAssetsByNameFilter, 34 fetchAssetsByNameFilter: fetchAssetsByNameFilter,
35 - getAssetTypes: getAssetTypes 35 + getAssetTypes: getAssetTypes,
  36 + findByName: findByName
36 } 37 }
37 38
38 return service; 39 return service;
@@ -276,4 +277,16 @@ function AssetService($http, $q, customerService, userService) { @@ -276,4 +277,16 @@ function AssetService($http, $q, customerService, userService) {
276 return deferred.promise; 277 return deferred.promise;
277 } 278 }
278 279
  280 + function findByName(assetName, config) {
  281 + config = config || {};
  282 + var deferred = $q.defer();
  283 + var url = '/api/tenant/assets?assetName=' + assetName;
  284 + $http.get(url, config).then(function success(response) {
  285 + deferred.resolve(response.data);
  286 + }, function fail() {
  287 + deferred.reject();
  288 + });
  289 + return deferred.promise;
  290 + }
  291 +
279 } 292 }
@@ -30,7 +30,9 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -30,7 +30,9 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
30 subscribeForEntityAttributes: subscribeForEntityAttributes, 30 subscribeForEntityAttributes: subscribeForEntityAttributes,
31 unsubscribeForEntityAttributes: unsubscribeForEntityAttributes, 31 unsubscribeForEntityAttributes: unsubscribeForEntityAttributes,
32 saveEntityAttributes: saveEntityAttributes, 32 saveEntityAttributes: saveEntityAttributes,
33 - deleteEntityAttributes: deleteEntityAttributes 33 + deleteEntityAttributes: deleteEntityAttributes,
  34 + saveEntityTimeseries: saveEntityTimeseries,
  35 + deleteEntityTimeseries: deleteEntityTimeseries
34 } 36 }
35 37
36 return service; 38 return service;
@@ -212,7 +214,8 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -212,7 +214,8 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
212 } 214 }
213 } 215 }
214 216
215 - function saveEntityAttributes(entityType, entityId, attributeScope, attributes) { 217 + function saveEntityAttributes(entityType, entityId, attributeScope, attributes, config) {
  218 + config = config || {};
216 var deferred = $q.defer(); 219 var deferred = $q.defer();
217 var attributesData = {}; 220 var attributesData = {};
218 var deleteAttributes = []; 221 var deleteAttributes = [];
@@ -229,7 +232,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -229,7 +232,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
229 } 232 }
230 if (Object.keys(attributesData).length) { 233 if (Object.keys(attributesData).length) {
231 var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope; 234 var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope;
232 - $http.post(url, attributesData).then(function success(response) { 235 + $http.post(url, attributesData, config).then(function success(response) {
233 if (deleteEntityAttributesPromise) { 236 if (deleteEntityAttributesPromise) {
234 deleteEntityAttributesPromise.then( 237 deleteEntityAttributesPromise.then(
235 function success() { 238 function success() {
@@ -260,7 +263,57 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -260,7 +263,57 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
260 return deferred.promise; 263 return deferred.promise;
261 } 264 }
262 265
263 - function deleteEntityAttributes(entityType, entityId, attributeScope, attributes) { 266 + function saveEntityTimeseries(entityType, entityId, timeseriesScope, timeseries, config) {
  267 + config = config || {};
  268 + var deferred = $q.defer();
  269 + var timeseriesData = {};
  270 + var deleteTimeseries = [];
  271 + for (var a=0; a<timeseries.length;a++) {
  272 + if (angular.isDefined(timeseries[a].value) && timeseries[a].value !== null) {
  273 + timeseriesData[timeseries[a].key] = timeseries[a].value;
  274 + } else {
  275 + deleteTimeseries.push(timeseries[a]);
  276 + }
  277 + }
  278 + var deleteEntityTimeseriesPromise;
  279 + if (deleteTimeseries.length) {
  280 + deleteEntityTimeseriesPromise = deleteEntityTimeseries(entityType, entityId, deleteTimeseries, config);
  281 + }
  282 + if (Object.keys(timeseriesData).length) {
  283 + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/' + timeseriesScope;
  284 + $http.post(url, timeseriesData, config).then(function success(response) {
  285 + if (deleteEntityTimeseriesPromise) {
  286 + deleteEntityTimeseriesPromise.then(
  287 + function success() {
  288 + deferred.resolve(response.data);
  289 + },
  290 + function fail() {
  291 + deferred.reject();
  292 + }
  293 + )
  294 + } else {
  295 + deferred.resolve(response.data);
  296 + }
  297 + }, function fail() {
  298 + deferred.reject();
  299 + });
  300 + } else if (deleteEntityTimeseriesPromise) {
  301 + deleteEntityTimeseriesPromise.then(
  302 + function success() {
  303 + deferred.resolve();
  304 + },
  305 + function fail() {
  306 + deferred.reject();
  307 + }
  308 + )
  309 + } else {
  310 + deferred.resolve();
  311 + }
  312 + return deferred.promise;
  313 + }
  314 +
  315 + function deleteEntityAttributes(entityType, entityId, attributeScope, attributes, config) {
  316 + config = config || {};
264 var deferred = $q.defer(); 317 var deferred = $q.defer();
265 var keys = ''; 318 var keys = '';
266 for (var i = 0; i < attributes.length; i++) { 319 for (var i = 0; i < attributes.length; i++) {
@@ -270,7 +323,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -270,7 +323,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
270 keys += attributes[i].key; 323 keys += attributes[i].key;
271 } 324 }
272 var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys; 325 var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys;
273 - $http.delete(url).then(function success() { 326 + $http.delete(url, config).then(function success() {
274 deferred.resolve(); 327 deferred.resolve();
275 }, function fail() { 328 }, function fail() {
276 deferred.reject(); 329 deferred.reject();
@@ -278,5 +331,23 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService) @@ -278,5 +331,23 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
278 return deferred.promise; 331 return deferred.promise;
279 } 332 }
280 333
  334 + function deleteEntityTimeseries(entityType, entityId, timeseries, config) {
  335 + config = config || {};
  336 + var deferred = $q.defer();
  337 + var keys = '';
  338 + for (var i = 0; i < timeseries.length; i++) {
  339 + if (i > 0) {
  340 + keys += ',';
  341 + }
  342 + keys += timeseries[i].key;
  343 + }
  344 + var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/delete' + '?keys=' + keys;
  345 + $http.delete(url, config).then(function success() {
  346 + deferred.resolve();
  347 + }, function fail() {
  348 + deferred.reject();
  349 + });
  350 + return deferred.promise;
  351 + }
281 352
282 } 353 }
@@ -42,8 +42,9 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -42,8 +42,9 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
42 sendOneWayRpcCommand: sendOneWayRpcCommand, 42 sendOneWayRpcCommand: sendOneWayRpcCommand,
43 sendTwoWayRpcCommand: sendTwoWayRpcCommand, 43 sendTwoWayRpcCommand: sendTwoWayRpcCommand,
44 findByQuery: findByQuery, 44 findByQuery: findByQuery,
45 - getDeviceTypes: getDeviceTypes  
46 - } 45 + getDeviceTypes: getDeviceTypes,
  46 + findByName: findByName
  47 + };
47 48
48 return service; 49 return service;
49 50
@@ -124,7 +125,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -124,7 +125,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
124 if (!config) { 125 if (!config) {
125 config = {}; 126 config = {};
126 } 127 }
127 - config = Object.assign(config, { ignoreErrors: ignoreErrors }); 128 + config = Object.assign(config, {ignoreErrors: ignoreErrors});
128 $http.get(url, config).then(function success(response) { 129 $http.get(url, config).then(function success(response) {
129 deferred.resolve(response.data); 130 deferred.resolve(response.data);
130 }, function fail(response) { 131 }, function fail(response) {
@@ -136,8 +137,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -136,8 +137,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
136 function getDevices(deviceIds, config) { 137 function getDevices(deviceIds, config) {
137 var deferred = $q.defer(); 138 var deferred = $q.defer();
138 var ids = ''; 139 var ids = '';
139 - for (var i=0;i<deviceIds.length;i++) {  
140 - if (i>0) { 140 + for (var i = 0; i < deviceIds.length; i++) {
  141 + if (i > 0) {
141 ids += ','; 142 ids += ',';
142 } 143 }
143 ids += deviceIds[i]; 144 ids += deviceIds[i];
@@ -146,11 +147,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -146,11 +147,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
146 $http.get(url, config).then(function success(response) { 147 $http.get(url, config).then(function success(response) {
147 var devices = response.data; 148 var devices = response.data;
148 devices.sort(function (device1, device2) { 149 devices.sort(function (device1, device2) {
149 - var id1 = device1.id.id;  
150 - var id2 = device2.id.id;  
151 - var index1 = deviceIds.indexOf(id1);  
152 - var index2 = deviceIds.indexOf(id2);  
153 - return index1 - index2; 150 + var id1 = device1.id.id;
  151 + var id2 = device2.id.id;
  152 + var index1 = deviceIds.indexOf(id1);
  153 + var index2 = deviceIds.indexOf(id2);
  154 + return index1 - index2;
154 }); 155 });
155 deferred.resolve(devices); 156 deferred.resolve(devices);
156 }, function fail(response) { 157 }, function fail(response) {
@@ -159,10 +160,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -159,10 +160,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
159 return deferred.promise; 160 return deferred.promise;
160 } 161 }
161 162
162 - function saveDevice(device) { 163 + function saveDevice(device, config) {
  164 + config = config || {};
163 var deferred = $q.defer(); 165 var deferred = $q.defer();
164 var url = '/api/device'; 166 var url = '/api/device';
165 - $http.post(url, device).then(function success(response) { 167 + $http.post(url, device, config).then(function success(response) {
166 deferred.resolve(response.data); 168 deferred.resolve(response.data);
167 }, function fail() { 169 }, function fail() {
168 deferred.reject(); 170 deferred.reject();
@@ -181,7 +183,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -181,7 +183,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
181 return deferred.promise; 183 return deferred.promise;
182 } 184 }
183 185
184 - function getDeviceCredentials(deviceId, sync) { 186 + function getDeviceCredentials(deviceId, sync, config) {
  187 + config = config || {};
185 var deferred = $q.defer(); 188 var deferred = $q.defer();
186 var url = '/api/device/' + deviceId + '/credentials'; 189 var url = '/api/device/' + deviceId + '/credentials';
187 if (sync) { 190 if (sync) {
@@ -196,7 +199,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -196,7 +199,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
196 deferred.reject(); 199 deferred.reject();
197 } 200 }
198 } else { 201 } else {
199 - $http.get(url, null).then(function success(response) { 202 + $http.get(url, config).then(function success(response) {
200 deferred.resolve(response.data); 203 deferred.resolve(response.data);
201 }, function fail() { 204 }, function fail() {
202 deferred.reject(); 205 deferred.reject();
@@ -205,10 +208,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -205,10 +208,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
205 return deferred.promise; 208 return deferred.promise;
206 } 209 }
207 210
208 - function saveDeviceCredentials(deviceCredentials) { 211 + function saveDeviceCredentials(deviceCredentials, config) {
  212 + config = config || {};
209 var deferred = $q.defer(); 213 var deferred = $q.defer();
210 var url = '/api/device/credentials'; 214 var url = '/api/device/credentials';
211 - $http.post(url, deviceCredentials).then(function success(response) { 215 + $http.post(url, deviceCredentials, config).then(function success(response) {
212 deferred.resolve(response.data); 216 deferred.resolve(response.data);
213 }, function fail() { 217 }, function fail() {
214 deferred.reject(); 218 deferred.reject();
@@ -297,7 +301,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -297,7 +301,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
297 if (!config) { 301 if (!config) {
298 config = {}; 302 config = {};
299 } 303 }
300 - config = Object.assign(config, { ignoreErrors: ignoreErrors }); 304 + config = Object.assign(config, {ignoreErrors: ignoreErrors});
301 $http.post(url, query, config).then(function success(response) { 305 $http.post(url, query, config).then(function success(response) {
302 deferred.resolve(response.data); 306 deferred.resolve(response.data);
303 }, function fail() { 307 }, function fail() {
@@ -317,4 +321,15 @@ function DeviceService($http, $q, $window, userService, attributeService, custom @@ -317,4 +321,15 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
317 return deferred.promise; 321 return deferred.promise;
318 } 322 }
319 323
  324 + function findByName(deviceName, config) {
  325 + config = config || {};
  326 + var deferred = $q.defer();
  327 + var url = '/api/tenant/devices?deviceName=' + deviceName;
  328 + $http.get(url, config).then(function success(response) {
  329 + deferred.resolve(response.data);
  330 + }, function fail() {
  331 + deferred.reject();
  332 + });
  333 + return deferred.promise;
  334 + }
320 } 335 }
@@ -38,6 +38,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -38,6 +38,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
38 createAlarmSourceFromSubscriptionInfo: createAlarmSourceFromSubscriptionInfo, 38 createAlarmSourceFromSubscriptionInfo: createAlarmSourceFromSubscriptionInfo,
39 getRelatedEntities: getRelatedEntities, 39 getRelatedEntities: getRelatedEntities,
40 saveRelatedEntity: saveRelatedEntity, 40 saveRelatedEntity: saveRelatedEntity,
  41 + saveEntityParameters: saveEntityParameters,
41 getRelatedEntity: getRelatedEntity, 42 getRelatedEntity: getRelatedEntity,
42 deleteRelatedEntity: deleteRelatedEntity, 43 deleteRelatedEntity: deleteRelatedEntity,
43 moveEntity: moveEntity, 44 moveEntity: moveEntity,
@@ -1072,6 +1073,119 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device @@ -1072,6 +1073,119 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
1072 return deferred.promise; 1073 return deferred.promise;
1073 } 1074 }
1074 1075
  1076 + function saveEntityRelation(entityType, entityId, entityRelation, config) {
  1077 + const deferred = $q.defer();
  1078 + let attributesType = Object.keys(types.attributesScope);
  1079 + let allPromise = [];
  1080 + let promise = "";
  1081 + if (entityRelation.accessToken !== "") {
  1082 + promise = deviceService.getDeviceCredentials(entityId.id, null, config).then(function (response) {
  1083 + response.credentialsId = entityRelation.accessToken;
  1084 + response.credentialsType = "ACCESS_TOKEN";
  1085 + response.credentialsValue = null;
  1086 + return deviceService.saveDeviceCredentials(response, config).catch(function () {
  1087 + return "error";
  1088 + });
  1089 + });
  1090 + allPromise.push(promise)
  1091 + }
  1092 + for (let i = 0; i < attributesType.length; i++) {
  1093 + let attribute = attributesType[i];
  1094 + if (entityRelation.attributes[attribute] && entityRelation.attributes[attribute].length !== 0) {
  1095 + promise = attributeService.saveEntityAttributes(entityType, entityId.id, types.attributesScope[attribute].value, entityRelation.attributes[attribute], config).catch(function () {
  1096 + return "error";
  1097 + });
  1098 + allPromise.push(promise);
  1099 + }
  1100 + }
  1101 + if (entityRelation.timeseries.length !== 0) {
  1102 + promise = attributeService.saveEntityTimeseries(entityType, entityId.id, "time", entityRelation.timeseries, config).catch(function(){
  1103 + return "error";
  1104 + });
  1105 + allPromise.push(promise);
  1106 + }
  1107 + $q.all(allPromise).then(function success(response) {
  1108 + let isResponseHasError = false;
  1109 + for(let i = 0; i < response.length; i++){
  1110 + if(response[i] === "error"){
  1111 + isResponseHasError = true;
  1112 + break;
  1113 + }
  1114 + }
  1115 + isResponseHasError ? deferred.reject() : deferred.resolve();
  1116 + });
  1117 + return deferred.promise;
  1118 + }
  1119 +
  1120 + function saveEntityParameters(entityType, entityParameters, update, config) {
  1121 + config = config || {};
  1122 + const deferred = $q.defer();
  1123 + let statisticalInfo = {};
  1124 + let newEntity = {
  1125 + name: entityParameters.name,
  1126 + type: entityParameters.type
  1127 + };
  1128 + let promise;
  1129 + switch (entityType) {
  1130 + case types.entityType.device:
  1131 + promise = deviceService.saveDevice(newEntity, config);
  1132 + break;
  1133 + case types.entityType.asset:
  1134 + promise = assetService.saveAsset(newEntity, true, config);
  1135 + break;
  1136 + }
  1137 +
  1138 + promise.then(function success(response) {
  1139 + saveEntityRelation(entityType, response.id, entityParameters, config).then(function success() {
  1140 + statisticalInfo.create = {
  1141 + entity: 1
  1142 + };
  1143 + deferred.resolve(statisticalInfo);
  1144 + }, function fail() {
  1145 + statisticalInfo.error = {
  1146 + entity: 1
  1147 + };
  1148 + deferred.resolve(statisticalInfo);
  1149 + });
  1150 + }, function fail() {
  1151 + if (update) {
  1152 + let findIdEntity;
  1153 + switch (entityType) {
  1154 + case types.entityType.device:
  1155 + findIdEntity = deviceService.findByName(entityParameters.name, config);
  1156 + break;
  1157 + case types.entityType.asset:
  1158 + findIdEntity = assetService.findByName(entityParameters.name, config);
  1159 + break;
  1160 + }
  1161 + findIdEntity.then(function success(response) {
  1162 + saveEntityRelation(entityType, response.id, entityParameters, config).then(function success() {
  1163 + statisticalInfo.update = {
  1164 + entity: 1
  1165 + };
  1166 + deferred.resolve(statisticalInfo);
  1167 + }, function fail() {
  1168 + statisticalInfo.error = {
  1169 + entity: 1
  1170 + };
  1171 + deferred.resolve(statisticalInfo);
  1172 + });
  1173 + }, function fail() {
  1174 + statisticalInfo.error = {
  1175 + entity: 1
  1176 + };
  1177 + deferred.resolve(statisticalInfo);
  1178 + });
  1179 + } else {
  1180 + statisticalInfo.error = {
  1181 + entity: 1
  1182 + };
  1183 + deferred.resolve(statisticalInfo);
  1184 + }
  1185 + });
  1186 + return deferred.promise;
  1187 + }
  1188 +
1075 function getRelatedEntity(entityId, keys, typeTranslatePrefix) { 1189 function getRelatedEntity(entityId, keys, typeTranslatePrefix) {
1076 var deferred = $q.defer(); 1190 var deferred = $q.defer();
1077 getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then( 1191 getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then(
@@ -54,6 +54,8 @@ import react from 'ngreact'; @@ -54,6 +54,8 @@ import react from 'ngreact';
54 import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; 54 import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
55 import 'ngFlowchart/dist/ngFlowchart'; 55 import 'ngFlowchart/dist/ngFlowchart';
56 import 'jstree/dist/jstree.min'; 56 import 'jstree/dist/jstree.min';
  57 +import 'material-steppers/dist/material-steppers';
  58 +import 'material-steppers/dist/material-steppers.css'
57 import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css'; 59 import 'jstree-bootstrap-theme/dist/themes/proton/style.min.css';
58 import 'typeface-roboto'; 60 import 'typeface-roboto';
59 import 'font-awesome/css/font-awesome.min.css'; 61 import 'font-awesome/css/font-awesome.min.css';
@@ -127,6 +129,7 @@ angular.module('thingsboard', [ @@ -127,6 +129,7 @@ angular.module('thingsboard', [
127 react.name, 129 react.name,
128 'flow', 130 'flow',
129 'flowchart', 131 'flowchart',
  132 + 'mdSteppers',
130 thingsboardThirdpartyFix, 133 thingsboardThirdpartyFix,
131 thingsboardTranslateHandler, 134 thingsboardTranslateHandler,
132 thingsboardLogin, 135 thingsboardLogin,
@@ -48,7 +48,7 @@ export function AssetCardController(types) { @@ -48,7 +48,7 @@ export function AssetCardController(types) {
48 48
49 /*@ngInject*/ 49 /*@ngInject*/
50 export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams, 50 export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams,
51 - $document, $mdDialog, $q, $translate, types) { 51 + $document, $mdDialog, $q, $translate, types, importExport) {
52 52
53 var customerId = $stateParams.customerId; 53 var customerId = $stateParams.customerId;
54 54
@@ -56,6 +56,29 @@ export function AssetController($rootScope, userService, assetService, customerS @@ -56,6 +56,29 @@ export function AssetController($rootScope, userService, assetService, customerS
56 56
57 var assetGroupActionsList = []; 57 var assetGroupActionsList = [];
58 58
  59 + var assetAddItemActionsList = [
  60 + {
  61 + onAction: function ($event) {
  62 + vm.grid.addItem($event);
  63 + },
  64 + name: function() { return $translate.instant('action.add') },
  65 + details: function() { return $translate.instant('asset.add-asset-text') },
  66 + icon: "insert_drive_file"
  67 + },
  68 + {
  69 + onAction: function ($event) {
  70 + importExport.importEntities($event, types.entityType.asset).then(
  71 + function() {
  72 + vm.grid.refreshList();
  73 + }
  74 + );
  75 + },
  76 + name: function() { return $translate.instant('action.import') },
  77 + details: function() { return $translate.instant('asset.import') },
  78 + icon: "file_upload"
  79 + }
  80 + ];
  81 +
59 var vm = this; 82 var vm = this;
60 83
61 vm.types = types; 84 vm.types = types;
@@ -77,6 +100,7 @@ export function AssetController($rootScope, userService, assetService, customerS @@ -77,6 +100,7 @@ export function AssetController($rootScope, userService, assetService, customerS
77 100
78 actionsList: assetActionsList, 101 actionsList: assetActionsList,
79 groupActionsList: assetGroupActionsList, 102 groupActionsList: assetGroupActionsList,
  103 + addItemActions: assetAddItemActionsList,
80 104
81 onGridInited: gridInited, 105 onGridInited: gridInited,
82 106
@@ -294,6 +318,8 @@ export function AssetController($rootScope, userService, assetService, customerS @@ -294,6 +318,8 @@ export function AssetController($rootScope, userService, assetService, customerS
294 } else if (vm.assetsScope === 'customer_user') { 318 } else if (vm.assetsScope === 'customer_user') {
295 vm.assetGridConfig.addItemAction = {}; 319 vm.assetGridConfig.addItemAction = {};
296 } 320 }
  321 + vm.assetGridConfig.addItemActions = [];
  322 +
297 } 323 }
298 324
299 vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction; 325 vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction;
@@ -350,6 +350,40 @@ export default angular.module('thingsboard.types', []) @@ -350,6 +350,40 @@ export default angular.module('thingsboard.types', [])
350 rulenode: "RULE_NODE", 350 rulenode: "RULE_NODE",
351 entityView: "ENTITY_VIEW" 351 entityView: "ENTITY_VIEW"
352 }, 352 },
  353 + importEntityColumnType: {
  354 + name: {
  355 + name: 'import.column-type.name',
  356 + value: 'name'
  357 + },
  358 + type: {
  359 + name: 'import.column-type.type',
  360 + value: 'type'
  361 + },
  362 + clientAttribute: {
  363 + name: 'import.column-type.client-attribute',
  364 + value: 'CLIENT_ATTRIBUTE'
  365 + },
  366 + sharedAttribute: {
  367 + name: 'import.column-type.shared-attribute',
  368 + value: 'SHARED_ATTRIBUTE'
  369 + },
  370 + serverAttribute: {
  371 + name: 'import.column-type.server-attribute',
  372 + value: 'SERVER_ATTRIBUTE'
  373 + },
  374 + timeseries: {
  375 + name: 'import.column-type.timeseries',
  376 + value: 'TIMESERIES'
  377 + },
  378 + entityField: {
  379 + name: 'import.column-type.entity-field',
  380 + value: 'ENTITY_FIELD'
  381 + },
  382 + accessToken: {
  383 + name: 'import.column-type.access-token',
  384 + value: 'ACCESS_TOKEN'
  385 + }
  386 + },
353 aliasEntityType: { 387 aliasEntityType: {
354 current_customer: "CURRENT_CUSTOMER" 388 current_customer: "CURRENT_CUSTOMER"
355 }, 389 },
@@ -49,7 +49,7 @@ export function DeviceCardController(types) { @@ -49,7 +49,7 @@ export function DeviceCardController(types) {
49 49
50 /*@ngInject*/ 50 /*@ngInject*/
51 export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams, 51 export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams,
52 - $document, $mdDialog, $q, $translate, types) { 52 + $document, $mdDialog, $q, $translate, types, importExport) {
53 53
54 var customerId = $stateParams.customerId; 54 var customerId = $stateParams.customerId;
55 55
@@ -57,6 +57,29 @@ export function DeviceController($rootScope, userService, deviceService, custome @@ -57,6 +57,29 @@ export function DeviceController($rootScope, userService, deviceService, custome
57 57
58 var deviceGroupActionsList = []; 58 var deviceGroupActionsList = [];
59 59
  60 + var deviceAddItemActionsList = [
  61 + {
  62 + onAction: function ($event) {
  63 + vm.grid.addItem($event);
  64 + },
  65 + name: function() { return $translate.instant('action.add') },
  66 + details: function() { return $translate.instant('device.add-device-text') },
  67 + icon: "insert_drive_file"
  68 + },
  69 + {
  70 + onAction: function ($event) {
  71 + importExport.importEntities($event, types.entityType.device).then(
  72 + function() {
  73 + vm.grid.refreshList();
  74 + }
  75 + );
  76 + },
  77 + name: function() { return $translate.instant('action.import') },
  78 + details: function() { return $translate.instant('device.import') },
  79 + icon: "file_upload"
  80 + }
  81 + ];
  82 +
60 var vm = this; 83 var vm = this;
61 84
62 vm.types = types; 85 vm.types = types;
@@ -78,12 +101,12 @@ export function DeviceController($rootScope, userService, deviceService, custome @@ -78,12 +101,12 @@ export function DeviceController($rootScope, userService, deviceService, custome
78 101
79 actionsList: deviceActionsList, 102 actionsList: deviceActionsList,
80 groupActionsList: deviceGroupActionsList, 103 groupActionsList: deviceGroupActionsList,
  104 + addItemActions: deviceAddItemActionsList,
81 105
82 onGridInited: gridInited, 106 onGridInited: gridInited,
83 107
84 addItemTemplateUrl: addDeviceTemplate, 108 addItemTemplateUrl: addDeviceTemplate,
85 109
86 - addItemText: function() { return $translate.instant('device.add-device-text') },  
87 noItemsText: function() { return $translate.instant('device.no-devices-text') }, 110 noItemsText: function() { return $translate.instant('device.no-devices-text') },
88 itemDetailsText: function() { return $translate.instant('device.device-details') }, 111 itemDetailsText: function() { return $translate.instant('device.device-details') },
89 isDetailsReadOnly: isCustomerUser, 112 isDetailsReadOnly: isCustomerUser,
@@ -314,7 +337,6 @@ export function DeviceController($rootScope, userService, deviceService, custome @@ -314,7 +337,6 @@ export function DeviceController($rootScope, userService, deviceService, custome
314 icon: "add" 337 icon: "add"
315 }; 338 };
316 339
317 -  
318 } else if (vm.devicesScope === 'customer_user') { 340 } else if (vm.devicesScope === 'customer_user') {
319 deviceActionsList.push( 341 deviceActionsList.push(
320 { 342 {
@@ -329,6 +351,8 @@ export function DeviceController($rootScope, userService, deviceService, custome @@ -329,6 +351,8 @@ export function DeviceController($rootScope, userService, deviceService, custome
329 351
330 vm.deviceGridConfig.addItemAction = {}; 352 vm.deviceGridConfig.addItemAction = {};
331 } 353 }
  354 + vm.deviceGridConfig.addItemActions = [];
  355 +
332 } 356 }
333 357
334 vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction; 358 vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction;
@@ -21,6 +21,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -21,6 +21,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
21 var userService; 21 var userService;
22 var types; 22 var types;
23 var http; 23 var http;
  24 + var timeout;
24 25
25 var internalUrlPrefixes = [ 26 var internalUrlPrefixes = [
26 '/api/auth/token', 27 '/api/auth/token',
@@ -71,6 +72,13 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -71,6 +72,13 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
71 return http; 72 return http;
72 } 73 }
73 74
  75 + function getTimeout() {
  76 + if (!timeout) {
  77 + timeout = $injector.get("$timeout");
  78 + }
  79 + return timeout;
  80 + }
  81 +
74 function rejectionErrorCode(rejection) { 82 function rejectionErrorCode(rejection) {
75 if (rejection && rejection.data && rejection.data.errorCode) { 83 if (rejection && rejection.data && rejection.data.errorCode) {
76 return rejection.data.errorCode; 84 return rejection.data.errorCode;
@@ -144,12 +152,20 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -144,12 +152,20 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
144 return response; 152 return response;
145 } 153 }
146 154
  155 + function retryRequest (httpConfig) {
  156 + var thisTimeout = 1000 + Math.random() * 3000;
  157 + return getTimeout()(function() {
  158 + return getHttp()(httpConfig);
  159 + }, thisTimeout);
  160 + }
  161 +
147 function responseError(rejection) { 162 function responseError(rejection) {
148 if (rejection.config.url.startsWith('/api/')) { 163 if (rejection.config.url.startsWith('/api/')) {
149 updateLoadingState(rejection.config, false); 164 updateLoadingState(rejection.config, false);
150 } 165 }
151 var unhandled = false; 166 var unhandled = false;
152 var ignoreErrors = rejection.config.ignoreErrors; 167 var ignoreErrors = rejection.config.ignoreErrors;
  168 + var resendRequest = rejection.config.resendRequest;
153 if (rejection.refreshTokenPending || rejection.status === 401) { 169 if (rejection.refreshTokenPending || rejection.status === 401) {
154 var errorCode = rejectionErrorCode(rejection); 170 var errorCode = rejectionErrorCode(rejection);
155 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { 171 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
@@ -161,6 +177,10 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -161,6 +177,10 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
161 if (!ignoreErrors) { 177 if (!ignoreErrors) {
162 $rootScope.$broadcast('forbidden'); 178 $rootScope.$broadcast('forbidden');
163 } 179 }
  180 + } else if (rejection.status === 429) {
  181 + if (resendRequest) {
  182 + return retryRequest(rejection.config);
  183 + }
164 } else if (rejection.status === 0 || rejection.status === -1) { 184 } else if (rejection.status === 0 || rejection.status === -1) {
165 getToast().showError(getTranslate().instant('error.unable-to-connect')); 185 getToast().showError(getTranslate().instant('error.unable-to-connect'));
166 } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) { 186 } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
@@ -96,6 +96,7 @@ export default angular.module('thingsboard.help', []) @@ -96,6 +96,7 @@ export default angular.module('thingsboard.help', [])
96 assets: helpBaseUrl + "/docs/user-guide/ui/assets", 96 assets: helpBaseUrl + "/docs/user-guide/ui/assets",
97 devices: helpBaseUrl + "/docs/user-guide/ui/devices", 97 devices: helpBaseUrl + "/docs/user-guide/ui/devices",
98 entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views", 98 entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views",
  99 + entitiesImport: helpBaseUrl + "/docs/user-guide/bulk-provisioning",
99 dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards", 100 dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards",
100 users: helpBaseUrl + "/docs/user-guide/ui/users", 101 users: helpBaseUrl + "/docs/user-guide/ui/users",
101 widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles", 102 widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles",
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './import-dialog.scss';
  17 +
  18 +/*@ngInject*/
  19 +export default function ImportDialogCsvController($scope, $mdDialog, toast, importTitle, importFileLabel, entityType, importExport, types, $mdStepper, $timeout) {
  20 +
  21 + var vm = this;
  22 +
  23 + vm.cancel = cancel;
  24 + vm.fileAdded = fileAdded;
  25 + vm.clearFile = clearFile;
  26 + vm.nextStep = nextStep;
  27 + vm.previousStep = previousStep;
  28 +
  29 + vm.importParameters = {
  30 + delim: ',',
  31 + isUpdate: true,
  32 + isHeader: true
  33 + };
  34 +
  35 + vm.importTitle = importTitle;
  36 + vm.importFileLabel = importFileLabel;
  37 + vm.entityType = entityType;
  38 +
  39 + vm.isVertical = true;
  40 + vm.isLinear = true;
  41 + vm.isAlternative = false;
  42 + vm.isMobileStepText = true;
  43 + vm.isImportData = false;
  44 +
  45 + vm.parseData = [];
  46 +
  47 + vm.delimiters = [{
  48 + key: ',',
  49 + value: ','
  50 + }, {
  51 + key: ';',
  52 + value: ';'
  53 + }, {
  54 + key: '|',
  55 + value: '|'
  56 + }, {
  57 + key: '\t',
  58 + value: 'Tab'
  59 + }];
  60 +
  61 + vm.progressCreate = 0;
  62 +
  63 + var parseData = {};
  64 +
  65 + function fileAdded($file) {
  66 + if ($file.getExtension() === 'csv') {
  67 + var reader = new FileReader();
  68 + reader.onload = function (event) {
  69 + $scope.$apply(function () {
  70 + if (event.target.result) {
  71 + vm.theFormStep1.$setDirty();
  72 + var importCSV = event.target.result;
  73 + if (importCSV && importCSV.length > 0) {
  74 + try {
  75 + vm.importData = importCSV;
  76 + vm.fileName = $file.name;
  77 + } catch (err) {
  78 + vm.fileName = null;
  79 + toast.showError(err.message);
  80 + }
  81 + }
  82 + }
  83 + });
  84 + };
  85 + reader.readAsText($file.file);
  86 + }
  87 + }
  88 +
  89 + function parseCSV(importData) {
  90 + var config = {
  91 + delim: vm.importParameters.delim,
  92 + header: vm.importParameters.isHeader
  93 + };
  94 + return importExport.convertCSVToJson(importData, config);
  95 + }
  96 +
  97 + function createColumnsData(parseData) {
  98 + vm.columnsParam = [];
  99 + var columnParam = {};
  100 + for (var i = 0; i < parseData.headers.length; i++) {
  101 + if (vm.importParameters.isHeader && parseData.headers[i].search(/^(name|type)$/im) === 0) {
  102 + columnParam = {
  103 + type: types.importEntityColumnType[parseData.headers[i].toLowerCase()].value,
  104 + key: parseData.headers[i].toLowerCase(),
  105 + sampleData: parseData.rows[0][i]
  106 + };
  107 + } else {
  108 + columnParam = {
  109 + type: types.importEntityColumnType.serverAttribute.value,
  110 + key: vm.importParameters.isHeader ? parseData.headers[i] : "",
  111 + sampleData: parseData.rows[0][i]
  112 + };
  113 + }
  114 + vm.columnsParam.push(columnParam);
  115 + }
  116 + }
  117 +
  118 + function addEntities (importData, parameterColumns) {
  119 + var entitiesData = [];
  120 + var sentDataLength = 0;
  121 + var config = {
  122 + ignoreErrors: true,
  123 + resendRequest: true
  124 + };
  125 + for (var i = 0; i < importData.rows.length; i++) {
  126 + var entityData = {
  127 + name: "",
  128 + type: "",
  129 + accessToken: "",
  130 + attributes: {
  131 + server: [],
  132 + shared: []
  133 + },
  134 + timeseries: []
  135 + };
  136 + for (var j = 0; j < parameterColumns.length; j++) {
  137 + switch (parameterColumns[j].type) {
  138 + case types.importEntityColumnType.serverAttribute.value:
  139 + entityData.attributes.server.push({
  140 + key: parameterColumns[j].key,
  141 + value: importData.rows[i][j]
  142 + });
  143 + break;
  144 + case types.importEntityColumnType.timeseries.value:
  145 + entityData.timeseries.push({
  146 + key: parameterColumns[j].key,
  147 + value: importData.rows[i][j]
  148 + });
  149 + break;
  150 + case types.importEntityColumnType.sharedAttribute.value:
  151 + entityData.attributes.shared.push({
  152 + key: parameterColumns[j].key,
  153 + value: importData.rows[i][j]
  154 + });
  155 + break;
  156 + case types.importEntityColumnType.accessToken.value:
  157 + entityData.accessToken = importData.rows[i][j];
  158 + break;
  159 + case types.importEntityColumnType.name.value:
  160 + entityData.name = importData.rows[i][j];
  161 + break;
  162 + case types.importEntityColumnType.type.value:
  163 + entityData.type = importData.rows[i][j];
  164 + break;
  165 + }
  166 + }
  167 + entitiesData.push(entityData);
  168 + }
  169 + $scope.$on('createImportEntityCompleted', function () {
  170 + sentDataLength++;
  171 + vm.progressCreate = Math.round((sentDataLength / importData.rows.length) * 100);
  172 + });
  173 + importExport.createMultiEntity(entitiesData, vm.entityType, vm.importParameters.isUpdate, config).then(function (response) {
  174 + vm.statistical = response;
  175 + vm.isImportData = false;
  176 + $mdStepper('import-stepper').next();
  177 + });
  178 + }
  179 +
  180 + function clearFile() {
  181 + vm.theFormStep1.$setDirty();
  182 + vm.fileName = null;
  183 + vm.importData = null;
  184 + }
  185 +
  186 + function previousStep(step) {
  187 + let steppers = $mdStepper('import-stepper');
  188 + switch (step) {
  189 + case 1:
  190 + steppers.back();
  191 + $timeout(function () {
  192 + vm.theFormStep1.$setDirty();
  193 + });
  194 + break;
  195 + default:
  196 + steppers.back();
  197 + break;
  198 + }
  199 + }
  200 +
  201 + function nextStep(step) {
  202 + let steppers = $mdStepper('import-stepper');
  203 + switch (step) {
  204 + case 2:
  205 + steppers.next();
  206 + break;
  207 + case 3:
  208 + parseData = parseCSV(vm.importData);
  209 + if (parseData === -1) {
  210 + steppers.back();
  211 + $timeout(function () {
  212 + clearFile();
  213 + });
  214 + } else {
  215 + createColumnsData(parseData);
  216 + steppers.next();
  217 + }
  218 + break;
  219 + case 4:
  220 + steppers.next();
  221 + vm.isImportData = true;
  222 + addEntities(parseData, vm.columnsParam);
  223 + break;
  224 + case 6:
  225 + $mdDialog.hide();
  226 + break;
  227 + }
  228 +
  229 + }
  230 +
  231 + function cancel() {
  232 + if($mdStepper('import-stepper').currentStep > 2){
  233 + $mdDialog.hide();
  234 + } else {
  235 + $mdDialog.cancel();
  236 + }
  237 + }
  238 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ vm.importTitle | translate }}" class="tb-import-stepper" tb-help="'entitiesImport'"
  19 + help-container-id="help-container">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>{{ vm.importTitle }}</h2>
  23 + <span flex></span>
  24 + <div id="help-container"></div>
  25 + <md-button class="md-icon-button" ng-click="vm.cancel()" ng-disabled="vm.isImportData">
  26 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  27 + </md-button>
  28 + </div>
  29 + </md-toolbar>
  30 +
  31 + <span style="max-height: 5px; height: 5px;" flex></span>
  32 + <md-dialog-content>
  33 + <md-stepper id="import-stepper" md-mobile-step-text="vm.isMobileStepText" md-vertical="vm.isVertical"
  34 + md-linear="vm.isLinear" md-alternative="vm.isAlternative">
  35 + <md-step md-label="{{ 'import.stepper-text.select-file' | translate }}">
  36 + <md-step-body>
  37 + <form name="vm.theFormStep1">
  38 + <fieldset ng-disabled="$root.loading">
  39 + <div layout="column" layout-padding>
  40 + <div class="tb-container">
  41 + <label class="tb-label" translate>{{ vm.importFileLabel }}</label>
  42 + <div flow-init="{singleFile:true}"
  43 + flow-file-added="vm.fileAdded( $file )" class="tb-file-select-container">
  44 + <div class="tb-file-clear-container">
  45 + <md-button ng-click="vm.clearFile()"
  46 + class="tb-file-clear-btn md-icon-button md-primary"
  47 + aria-label="{{ 'action.remove' | translate }}">
  48 + <md-tooltip md-direction="top">
  49 + {{ 'action.remove' | translate }}
  50 + </md-tooltip>
  51 + <md-icon aria-label="{{ 'action.remove' | translate }}"
  52 + class="material-icons">
  53 + close
  54 + </md-icon>
  55 + </md-button>
  56 + </div>
  57 + <div class="alert tb-flow-drop" flow-drop>
  58 + <label for="select" translate>import.drop-file-csv</label>
  59 + <input class="file-input" flow-btn
  60 + flow-attrs="{accept:'.csv,application/csv,text/csv'}"
  61 + id="select">
  62 + </div>
  63 + </div>
  64 + </div>
  65 + <div>
  66 + <div ng-show="!vm.fileName" translate>import.no-file</div>
  67 + <div ng-show="vm.fileName">{{ vm.fileName }}</div>
  68 + </div>
  69 + </div>
  70 + </fieldset>
  71 + </form>
  72 + </md-step-body>
  73 +
  74 + <md-step-actions layout="row">
  75 + <span flex></span>
  76 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()">
  77 + {{ 'action.cancel' | translate }}
  78 + </md-button>
  79 + <md-button class="md-primary md-raised"
  80 + ng-disabled="$root.loading || !vm.theFormStep1.$dirty || !vm.theFormStep1.$valid || !vm.importData"
  81 + ng-click="vm.nextStep(2);">
  82 + {{ 'action.continue' | translate }}
  83 + </md-button>
  84 + </md-step-actions>
  85 + </md-step>
  86 +
  87 + <md-step md-label="{{ 'import.stepper-text.configuration' | translate }}">
  88 + <md-step-body>
  89 + <div layout="column">
  90 + <md-input-container>
  91 + <label translate>import.csv-delimiter</label>
  92 + <md-select ng-model="vm.importParameters.delim">
  93 + <md-option ng-repeat="delimiter in vm.delimiters" ng-value="delimiter.key">
  94 + {{delimiter.value}}
  95 + </md-option>
  96 + </md-select>
  97 + </md-input-container>
  98 + <md-checkbox ng-model="vm.importParameters.isHeader"
  99 + aria-label="First line contains column names">
  100 + {{ 'import.csv-first-line-header' | translate }}
  101 + </md-checkbox>
  102 + <md-checkbox ng-model="vm.importParameters.isUpdate" aria-label="Update attributes/telemetry">
  103 + {{ 'import.csv-update-data' | translate }}
  104 + </md-checkbox>
  105 + </div>
  106 + </md-step-body>
  107 +
  108 + <md-step-actions layout="row">
  109 + <md-button ng-disabled="$root.loading" ng-click="vm.previousStep(1);">Back
  110 + </md-button>
  111 + <span flex></span>
  112 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()">
  113 + {{ 'action.cancel' | translate }}
  114 + </md-button>
  115 + <md-button class="md-primary md-raised" ng-disabled="$root.loading" ng-click="vm.nextStep(3);">
  116 + {{ 'action.continue' | translate }}
  117 + </md-button>
  118 + </md-step-actions>
  119 + </md-step>
  120 +
  121 + <md-step md-label="{{ 'import.stepper-text.column-type' | translate }}">
  122 + <md-step-body>
  123 + <form name="vm.theFormStep3">
  124 + <tb-table-columns-assignment columns="vm.columnsParam" the-form="vm.theFormStep3"
  125 + entity-type="vm.entityType"></tb-table-columns-assignment>
  126 + </form>
  127 + </md-step-body>
  128 +
  129 + <md-step-actions layout="row">
  130 + <md-button ng-disabled="$root.loading" ng-click="vm.previousStep();">Back
  131 + </md-button>
  132 + <span flex></span>
  133 + <md-button ng-disabled="$root.loading" ng-click="vm.cancel()">
  134 + {{ 'action.cancel' | translate }}
  135 + </md-button>
  136 + <md-button class="md-primary md-raised"
  137 + ng-disabled="$root.loading || !vm.theFormStep3.$dirty || !vm.theFormStep3.$valid"
  138 + ng-click="vm.nextStep(4);">
  139 + {{ 'action.continue' | translate }}
  140 + </md-button>
  141 + </md-step-actions>
  142 + </md-step>
  143 +
  144 + <md-step md-label="{{ 'import.stepper-text.creat-entities' | translate }}">
  145 + <md-step-body>
  146 + <md-progress-linear class="md-warn tb-import-progress" md-mode="determinate"
  147 + value="{{vm.progressCreate}}"></md-progress-linear>
  148 + </md-step-body>
  149 + </md-step>
  150 + <md-step md-label="{{ 'import.stepper-text.done' | translate }}">
  151 + <md-step-body layout="column">
  152 + <div>
  153 + <p class="md-body-1" translate translate-values="{count: vm.statistical.create.entity}"
  154 + ng-if="vm.statistical.create && vm.statistical.create.entity">import.message.create-entities</p>
  155 + <p class="md-body-1" translate translate-values="{count: vm.statistical.update.entity}"
  156 + ng-if="vm.statistical.update && vm.statistical.update.entity">import.message.update-entities</p>
  157 + <p class="md-body-1" translate translate-values="{count: vm.statistical.error.entity}"
  158 + ng-if="vm.statistical.error && vm.statistical.error.entity">import.message.error-entities</p>
  159 + </div>
  160 + </md-step-body>
  161 + <md-step-actions layout="row">
  162 + <span flex></span>
  163 + <md-button class="md-primary md-raised" ng-disabled="$root.loading" ng-click="vm.nextStep(6);">
  164 + {{ 'action.ok' | translate }}
  165 + </md-button>
  166 + </md-step-actions>
  167 + </md-step>
  168 +
  169 + </md-stepper>
  170 + </md-dialog-content>
  171 +</md-dialog>
@@ -13,6 +13,8 @@ @@ -13,6 +13,8 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
  16 +@import "../../scss/constants.scss";
  17 +
16 $previewSize: 100px !default; 18 $previewSize: 100px !default;
17 19
18 .tb-file-select-container { 20 .tb-file-select-container {
@@ -40,3 +42,21 @@ $previewSize: 100px !default; @@ -40,3 +42,21 @@ $previewSize: 100px !default;
40 top: 50%; 42 top: 50%;
41 transform: translate(0%, -50%) !important; 43 transform: translate(0%, -50%) !important;
42 } 44 }
  45 +
  46 +.tb-table-select{
  47 + md-input-container{
  48 + margin: 0;
  49 +
  50 + .md-errors-spacer{
  51 + min-height: 0;
  52 + }
  53 + }
  54 +}
  55 +
  56 +.md-stepper-indicator-wrapper{
  57 + background-color: transparent;
  58 +}
  59 +
  60 +.tb-import-progress{
  61 + margin: 7px 0;
  62 +}
@@ -13,9 +13,10 @@ @@ -13,9 +13,10 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -/* eslint-disable import/no-unresolved, import/default */ 16 +/* eslint-disable import/no-unresolved, import/default*/
17 17
18 import importDialogTemplate from './import-dialog.tpl.html'; 18 import importDialogTemplate from './import-dialog.tpl.html';
  19 +import importDialogCSVTemplate from './import-dialog-csv.tpl.html';
19 import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; 20 import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
20 21
21 /* eslint-enable import/no-unresolved, import/default */ 22 /* eslint-enable import/no-unresolved, import/default */
@@ -24,7 +25,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; @@ -24,7 +25,7 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
24 /* eslint-disable no-undef, angular/window-service, angular/document-service */ 25 /* eslint-disable no-undef, angular/window-service, angular/document-service */
25 26
26 /*@ngInject*/ 27 /*@ngInject*/
27 -export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, 28 +export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types, $rootScope,
28 dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) { 29 dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) {
29 30
30 31
@@ -41,7 +42,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -41,7 +42,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
41 importWidgetsBundle: importWidgetsBundle, 42 importWidgetsBundle: importWidgetsBundle,
42 exportExtension: exportExtension, 43 exportExtension: exportExtension,
43 importExtension: importExtension, 44 importExtension: importExtension,
44 - exportToPc: exportToPc 45 + importEntities: importEntities,
  46 + convertCSVToJson: convertCSVToJson,
  47 + exportToPc: exportToPc,
  48 + createMultiEntity: createMultiEntity
45 }; 49 };
46 50
47 return service; 51 return service;
@@ -54,11 +58,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -54,11 +58,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
54 var bundleAlias = widgetsBundle.alias; 58 var bundleAlias = widgetsBundle.alias;
55 var isSystem = widgetsBundle.tenantId.id === types.id.nullUid; 59 var isSystem = widgetsBundle.tenantId.id === types.id.nullUid;
56 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( 60 widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
57 - function success (widgetTypes) { 61 + function success(widgetTypes) {
58 prepareExport(widgetsBundle); 62 prepareExport(widgetsBundle);
59 var widgetsBundleItem = { 63 var widgetsBundleItem = {
60 - widgetsBundle: prepareExport(widgetsBundle),  
61 - widgetTypes: [] 64 + widgetsBundle: prepareExport(widgetsBundle),
  65 + widgetTypes: []
62 }; 66 };
63 for (var t in widgetTypes) { 67 for (var t in widgetTypes) {
64 var widgetType = widgetTypes[t]; 68 var widgetType = widgetTypes[t];
@@ -68,10 +72,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -68,10 +72,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
68 widgetsBundleItem.widgetTypes.push(prepareExport(widgetType)); 72 widgetsBundleItem.widgetTypes.push(prepareExport(widgetType));
69 } 73 }
70 var name = widgetsBundle.title; 74 var name = widgetsBundle.title;
71 - name = name.toLowerCase().replace(/\W/g,"_"); 75 + name = name.toLowerCase().replace(/\W/g, "_");
72 exportToPc(widgetsBundleItem, name + '.json'); 76 exportToPc(widgetsBundleItem, name + '.json');
73 }, 77 },
74 - function fail (rejection) { 78 + function fail(rejection) {
75 var message = rejection; 79 var message = rejection;
76 if (!message) { 80 if (!message) {
77 message = $translate.instant('error.unknown-error'); 81 message = $translate.instant('error.unknown-error');
@@ -168,7 +172,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -168,7 +172,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
168 delete widgetType.bundleAlias; 172 delete widgetType.bundleAlias;
169 } 173 }
170 var name = widgetType.name; 174 var name = widgetType.name;
171 - name = name.toLowerCase().replace(/\W/g,"_"); 175 + name = name.toLowerCase().replace(/\W/g, "_");
172 exportToPc(prepareExport(widgetType), name + '.json'); 176 exportToPc(prepareExport(widgetType), name + '.json');
173 }, 177 },
174 function fail(rejection) { 178 function fail(rejection) {
@@ -209,8 +213,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -209,8 +213,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
209 213
210 function validateImportedWidgetType(widgetType) { 214 function validateImportedWidgetType(widgetType) {
211 if (angular.isUndefined(widgetType.name) 215 if (angular.isUndefined(widgetType.name)
212 - || angular.isUndefined(widgetType.descriptor))  
213 - { 216 + || angular.isUndefined(widgetType.descriptor)) {
214 return false; 217 return false;
215 } 218 }
216 return true; 219 return true;
@@ -228,7 +231,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -228,7 +231,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
228 metadata: prepareRuleChainMetaData(ruleChainMetaData) 231 metadata: prepareRuleChainMetaData(ruleChainMetaData)
229 }; 232 };
230 var name = ruleChain.name; 233 var name = ruleChain.name;
231 - name = name.toLowerCase().replace(/\W/g,"_"); 234 + name = name.toLowerCase().replace(/\W/g, "_");
232 exportToPc(ruleChainExport, name + '.json'); 235 exportToPc(ruleChainExport, name + '.json');
233 }, 236 },
234 (rejection) => { 237 (rejection) => {
@@ -253,7 +256,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -253,7 +256,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
253 256
254 function prepareRuleChainMetaData(ruleChainMetaData) { 257 function prepareRuleChainMetaData(ruleChainMetaData) {
255 delete ruleChainMetaData.ruleChainId; 258 delete ruleChainMetaData.ruleChainId;
256 - for (var i=0;i<ruleChainMetaData.nodes.length;i++) { 259 + for (var i = 0; i < ruleChainMetaData.nodes.length; i++) {
257 var node = ruleChainMetaData.nodes[i]; 260 var node = ruleChainMetaData.nodes[i];
258 delete node.ruleChainId; 261 delete node.ruleChainId;
259 ruleChainMetaData.nodes[i] = prepareExport(node); 262 ruleChainMetaData.nodes[i] = prepareExport(node);
@@ -305,7 +308,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -305,7 +308,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
305 function exportWidget(dashboard, sourceState, sourceLayout, widget) { 308 function exportWidget(dashboard, sourceState, sourceLayout, widget) {
306 var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); 309 var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
307 var name = widgetItem.widget.config.title; 310 var name = widgetItem.widget.config.title;
308 - name = name.toLowerCase().replace(/\W/g,"_"); 311 + name = name.toLowerCase().replace(/\W/g, "_");
309 exportToPc(prepareExport(widgetItem), name + '.json'); 312 exportToPc(prepareExport(widgetItem), name + '.json');
310 } 313 }
311 314
@@ -415,10 +418,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -415,10 +418,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
415 var aliasIds = Object.keys(entityAliases); 418 var aliasIds = Object.keys(entityAliases);
416 if (aliasIds.length > 0) { 419 if (aliasIds.length > 0) {
417 processEntityAliases(entityAliases, aliasIds).then( 420 processEntityAliases(entityAliases, aliasIds).then(
418 - function(missingEntityAliases) { 421 + function (missingEntityAliases) {
419 if (Object.keys(missingEntityAliases).length > 0) { 422 if (Object.keys(missingEntityAliases).length > 0) {
420 - editMissingAliases($event, [ widget ],  
421 - true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then( 423 + editMissingAliases($event, [widget],
  424 + true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then(
422 function success(updatedEntityAliases) { 425 function success(updatedEntityAliases) {
423 for (var aliasId in updatedEntityAliases) { 426 for (var aliasId in updatedEntityAliases) {
424 var entityAlias = updatedEntityAliases[aliasId]; 427 var entityAlias = updatedEntityAliases[aliasId];
@@ -483,14 +486,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -483,14 +486,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
483 function success(targetLayout) { 486 function success(targetLayout) {
484 itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget, 487 itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget,
485 aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then( 488 aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then(
486 - function() {  
487 - deferred.resolve(  
488 - {  
489 - widget: widget,  
490 - layoutId: targetLayout  
491 - }  
492 - );  
493 - } 489 + function () {
  490 + deferred.resolve(
  491 + {
  492 + widget: widget,
  493 + layoutId: targetLayout
  494 + }
  495 + );
  496 + }
494 ); 497 );
495 }, 498 },
496 function fail() { 499 function fail() {
@@ -505,7 +508,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -505,7 +508,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
505 dashboardService.getDashboard(dashboardId).then( 508 dashboardService.getDashboard(dashboardId).then(
506 function success(dashboard) { 509 function success(dashboard) {
507 var name = dashboard.title; 510 var name = dashboard.title;
508 - name = name.toLowerCase().replace(/\W/g,"_"); 511 + name = name.toLowerCase().replace(/\W/g, "_");
509 exportToPc(prepareDashboardExport(dashboard), name + '.json'); 512 exportToPc(prepareDashboardExport(dashboard), name + '.json');
510 }, 513 },
511 function fail(rejection) { 514 function fail(rejection) {
@@ -538,13 +541,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -538,13 +541,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
538 dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); 541 dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
539 var entityAliases = dashboard.configuration.entityAliases; 542 var entityAliases = dashboard.configuration.entityAliases;
540 if (entityAliases) { 543 if (entityAliases) {
541 - var aliasIds = Object.keys( entityAliases ); 544 + var aliasIds = Object.keys(entityAliases);
542 if (aliasIds.length > 0) { 545 if (aliasIds.length > 0) {
543 processEntityAliases(entityAliases, aliasIds).then( 546 processEntityAliases(entityAliases, aliasIds).then(
544 - function(missingEntityAliases) {  
545 - if (Object.keys( missingEntityAliases ).length > 0) { 547 + function (missingEntityAliases) {
  548 + if (Object.keys(missingEntityAliases).length > 0) {
546 editMissingAliases($event, dashboard.configuration.widgets, 549 editMissingAliases($event, dashboard.configuration.widgets,
547 - false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then( 550 + false, 'dashboard.dashboard-import-missing-aliases-title', missingEntityAliases).then(
548 function success(updatedEntityAliases) { 551 function success(updatedEntityAliases) {
549 for (var aliasId in updatedEntityAliases) { 552 for (var aliasId in updatedEntityAliases) {
550 entityAliases[aliasId] = updatedEntityAliases[aliasId]; 553 entityAliases[aliasId] = updatedEntityAliases[aliasId];
@@ -575,6 +578,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -575,6 +578,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
575 return deferred.promise; 578 return deferred.promise;
576 } 579 }
577 580
  581 + function importEntities($event, entityType) {
  582 + var deferred = $q.defer();
  583 +
  584 + switch (entityType) {
  585 + case types.entityType.device:
  586 + openImportDialogCSV($event, entityType, 'device.import', 'device.device-file').then(
  587 + function success() {
  588 + deferred.resolve();
  589 + },
  590 + function fail() {
  591 + deferred.reject();
  592 + }
  593 + );
  594 + return deferred.promise;
  595 + case types.entityType.asset:
  596 + openImportDialogCSV($event, entityType, 'asset.import', 'asset.asset-file').then(
  597 + function success() {
  598 + deferred.resolve();
  599 + },
  600 + function fail() {
  601 + deferred.reject();
  602 + }
  603 + );
  604 + return deferred.promise;
  605 + }
  606 +
  607 + }
  608 +
578 function saveImportedDashboard(dashboard, deferred) { 609 function saveImportedDashboard(dashboard, deferred) {
579 dashboardService.saveDashboard(dashboard).then( 610 dashboardService.saveDashboard(dashboard).then(
580 function success() { 611 function success() {
@@ -594,14 +625,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -594,14 +625,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
594 } 625 }
595 626
596 627
597 -  
598 function exportExtension(extensionId) { 628 function exportExtension(extensionId) {
599 629
600 getExtension(extensionId) 630 getExtension(extensionId)
601 .then( 631 .then(
602 function success(extension) { 632 function success(extension) {
603 var name = extension.title; 633 var name = extension.title;
604 - name = name.toLowerCase().replace(/\W/g,"_"); 634 + name = name.toLowerCase().replace(/\W/g, "_");
605 exportToPc(prepareExport(extension), name + '.json'); 635 exportToPc(prepareExport(extension), name + '.json');
606 }, 636 },
607 function fail(rejection) { 637 function fail(rejection) {
@@ -661,7 +691,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -661,7 +691,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
661 function validateImportedExtension(configuration) { 691 function validateImportedExtension(configuration) {
662 if (configuration.length) { 692 if (configuration.length) {
663 for (let i = 0; i < configuration.length; i++) { 693 for (let i = 0; i < configuration.length; i++) {
664 - if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) { 694 + if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id) || angular.isUndefined(configuration[i].type)) {
665 return false; 695 return false;
666 } 696 }
667 } 697 }
@@ -692,7 +722,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -692,7 +722,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
692 var aliasId = aliasIds[index]; 722 var aliasId = aliasIds[index];
693 var entityAlias = entityAliases[aliasId]; 723 var entityAlias = entityAliases[aliasId];
694 entityService.checkEntityAlias(entityAlias).then( 724 entityService.checkEntityAlias(entityAlias).then(
695 - function(result) { 725 + function (result) {
696 if (result) { 726 if (result) {
697 checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); 727 checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred);
698 } else { 728 } else {
@@ -732,6 +762,124 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -732,6 +762,124 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
732 return deferred.promise; 762 return deferred.promise;
733 } 763 }
734 764
  765 + function splitCSV(str, sep) {
  766 + for (var foo = str.split(sep = sep || ","), x = foo.length - 1, tl; x >= 0; x--) {
  767 + if (foo[x].replace(/"\s+$/, '"').charAt(foo[x].length - 1) == '"') {
  768 + if ((tl = foo[x].replace(/^\s+"/, '"')).length > 1 && tl.charAt(0) == '"') {
  769 + foo[x] = foo[x].replace(/^\s*"|"\s*$/g, '').replace(/""/g, '"');
  770 + } else if (x) {
  771 + foo.splice(x - 1, 2, [foo[x - 1], foo[x]].join(sep));
  772 + } else foo = foo.shift().split(sep).concat(foo);
  773 + } else foo[x].replace(/""/g, '"');
  774 + }
  775 + return foo;
  776 + }
  777 +
  778 + function isNumeric(str) {
  779 + str = str.replace(',', '.');
  780 + return !isNaN(parseFloat(str)) && isFinite(str);
  781 + }
  782 +
  783 + function convertStringToJSType(str) {
  784 + if (isNumeric(str.replace(',', '.'))) {
  785 + return parseFloat(str.replace(',', '.'));
  786 + }
  787 + if (str.search(/^(true|false)$/im) === 0) {
  788 + return str === 'true';
  789 + }
  790 + if (str === "") {
  791 + return null;
  792 + }
  793 + return str;
  794 + }
  795 +
  796 + function convertCSVToJson(csvdata, config) {
  797 + config = config || {};
  798 + const delim = config.delim || ",";
  799 + const header = config.header || false;
  800 + let result = {};
  801 +
  802 + let csvlines = csvdata.split(/[\r\n]+/);
  803 + let csvheaders = splitCSV(csvlines[0], delim);
  804 + if (csvheaders.length < 2) {
  805 + toast.showError($translate.instant('import.import-csv-number-columns-error'));
  806 + return -1;
  807 + }
  808 + let csvrows = header ? csvlines.slice(1, csvlines.length) : csvlines;
  809 +
  810 + result.headers = csvheaders;
  811 + result.rows = [];
  812 +
  813 + for (let r in csvrows) {
  814 + if (csvrows.hasOwnProperty(r)) {
  815 + let row = csvrows[r];
  816 +
  817 + if (row.length === 0)
  818 + break;
  819 +
  820 + let rowitems = splitCSV(row, delim);
  821 + if (rowitems.length !== result.headers.length) {
  822 + toast.showError($translate.instant('import.import-csv-invalid-format-error', {line: (header ? result.rows.length + 2: result.rows.length + 1)}));
  823 + return -1;
  824 + }
  825 + for (let i = 0; i < rowitems.length; i++) {
  826 + rowitems[i] = convertStringToJSType(rowitems[i]);
  827 + }
  828 + result.rows.push(rowitems);
  829 + }
  830 + }
  831 + return result;
  832 + }
  833 +
  834 + function sumObject(obj1, obj2){
  835 + Object.keys(obj2).map(function(key) {
  836 + if (angular.isObject(obj2[key])) {
  837 + obj1[key] = obj1[key] || {};
  838 + angular.merge(obj1[key], sumObject(obj1[key], obj2[key]));
  839 + } else {
  840 + obj1[key] = (obj1[key] || 0) + obj2[key];
  841 + }
  842 + });
  843 + return obj1;
  844 + }
  845 +
  846 + function qAllWithProgress(promises) {
  847 + promises.forEach(function(p) {
  848 + p.then(function() {
  849 + $rootScope.$broadcast('createImportEntityCompleted', {});
  850 + });
  851 + });
  852 + return $q.all(promises);
  853 + }
  854 +
  855 + function createMultiEntity(arrayData, entityType, updateData, config) {
  856 + let partSize = 100;
  857 + partSize = arrayData.length > partSize ? partSize : arrayData.length;
  858 + let allPromise = [];
  859 + let statisticalInfo = {};
  860 + let deferred = $q.defer();
  861 +
  862 + for(let i = 0; i < partSize; i++){
  863 + const promise = entityService.saveEntityParameters(entityType, arrayData[i], updateData, config);
  864 + allPromise.push(promise);
  865 + }
  866 +
  867 + qAllWithProgress(allPromise).then(function success(response) {
  868 + for (let i = 0; i < response.length; i++){
  869 + statisticalInfo = sumObject(statisticalInfo, response[i]);
  870 + }
  871 + arrayData.splice(0, partSize);
  872 + if(arrayData.length > 0){
  873 + deferred.resolve(createMultiEntity(arrayData, entityType, updateData, config).then(function (response) {
  874 + return sumObject(statisticalInfo, response);
  875 + }));
  876 + } else {
  877 + deferred.resolve(statisticalInfo);
  878 + }
  879 + });
  880 + return deferred.promise;
  881 + }
  882 +
735 // Common functions 883 // Common functions
736 884
737 function prepareExport(data) { 885 function prepareExport(data) {
@@ -771,8 +919,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -771,8 +919,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
771 919
772 if (window.navigator && window.navigator.msSaveOrOpenBlob) { 920 if (window.navigator && window.navigator.msSaveOrOpenBlob) {
773 window.navigator.msSaveOrOpenBlob(blob, filename); 921 window.navigator.msSaveOrOpenBlob(blob, filename);
774 - }  
775 - else{ 922 + } else {
776 var e = document.createEvent('MouseEvents'), 923 var e = document.createEvent('MouseEvents'),
777 a = document.createElement('a'); 924 a = document.createElement('a');
778 925
@@ -807,6 +954,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document, @@ -807,6 +954,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
807 return deferred.promise; 954 return deferred.promise;
808 } 955 }
809 956
  957 + function openImportDialogCSV($event, entityType, importTitle, importFileLabel) {
  958 + var deferred = $q.defer();
  959 + $mdDialog.show({
  960 + controller: 'ImportDialogCSVController',
  961 + controllerAs: 'vm',
  962 + templateUrl: importDialogCSVTemplate,
  963 + locals: {
  964 + importTitle: importTitle,
  965 + importFileLabel: importFileLabel,
  966 + entityType: entityType
  967 + },
  968 + parent: angular.element($document[0].body),
  969 + onComplete: fixedDialogSize,
  970 + multiple: true,
  971 + fullscreen: true,
  972 + targetEvent: $event
  973 + }).then(function () {
  974 + deferred.resolve();
  975 + }, function () {
  976 + deferred.reject();
  977 + });
  978 + return deferred.promise;
  979 + }
  980 +
  981 + function fixedDialogSize(scope, element) {
  982 + let dialogElement = element[0].getElementsByTagName('md-dialog');
  983 + dialogElement[0].style.width = dialogElement[0].offsetWidth + 2 + "px";
  984 + }
810 } 985 }
811 986
812 /* eslint-enable no-undef, angular/window-service, angular/document-service */ 987 /* eslint-enable no-undef, angular/window-service, angular/document-service */
@@ -15,9 +15,13 @@ @@ -15,9 +15,13 @@
15 */ 15 */
16 import ImportExport from './import-export.service'; 16 import ImportExport from './import-export.service';
17 import ImportDialogController from './import-dialog.controller'; 17 import ImportDialogController from './import-dialog.controller';
  18 +import ImportDialogCSVController from './import-dialog-csv.controller';
  19 +import TableColumnsAssignment from './table-columns-assignment.directive';
18 20
19 21
20 export default angular.module('thingsboard.importexport', []) 22 export default angular.module('thingsboard.importexport', [])
21 .factory('importExport', ImportExport) 23 .factory('importExport', ImportExport)
22 .controller('ImportDialogController', ImportDialogController) 24 .controller('ImportDialogController', ImportDialogController)
  25 + .controller('ImportDialogCSVController', ImportDialogCSVController)
  26 + .directive('tbTableColumnsAssignment', TableColumnsAssignment)
23 .name; 27 .name;
  1 +/*
  2 + * Copyright © 2016-2019 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import tableColumnsAssignment from './table-columns-assignment.tpl.html';
  20 +
  21 +/* eslint-enable import/no-unresolved, import/default */
  22 +
  23 +/*@ngInject*/
  24 +export default function TableColumnsAssignment() {
  25 + return {
  26 + restrict: "E",
  27 + scope: true,
  28 + bindToController: {
  29 + theForm: '=?',
  30 + columns: '=',
  31 + entityType: '=',
  32 + },
  33 + templateUrl: tableColumnsAssignment,
  34 + controller: TableColumnsAssignmentController,
  35 + controllerAs: 'vm'
  36 + };
  37 +}
  38 +
  39 +/*@ngInject*/
  40 +function TableColumnsAssignmentController($scope, types, $timeout) {
  41 + var vm = this;
  42 +
  43 + vm.columnTypes = {};
  44 +
  45 + vm.columnTypes.name = types.importEntityColumnType.name;
  46 + vm.columnTypes.type = types.importEntityColumnType.type;
  47 +
  48 + switch (vm.entityType) {
  49 + case types.entityType.device:
  50 + vm.columnTypes.sharedAttribute = types.importEntityColumnType.sharedAttribute;
  51 + vm.columnTypes.serverAttribute = types.importEntityColumnType.serverAttribute;
  52 + vm.columnTypes.timeseries = types.importEntityColumnType.timeseries;
  53 + vm.columnTypes.accessToken = types.importEntityColumnType.accessToken;
  54 + break;
  55 + case types.entityType.asset:
  56 + vm.columnTypes.serverAttribute = types.importEntityColumnType.serverAttribute;
  57 + vm.columnTypes.timeseries = types.importEntityColumnType.timeseries;
  58 + break;
  59 + }
  60 +
  61 + $scope.$watch('vm.columns', function(newVal){
  62 + if (newVal) {
  63 + var isSelectName = false;
  64 + var isSelectType = false;
  65 + var isSelectCredentials = false;
  66 + for (var i = 0; i < newVal.length; i++) {
  67 + switch (newVal[i].type) {
  68 + case types.importEntityColumnType.name.value:
  69 + isSelectName = true;
  70 + break;
  71 + case types.importEntityColumnType.type.value:
  72 + isSelectType = true;
  73 + break;
  74 + case types.importEntityColumnType.accessToken.value:
  75 + isSelectCredentials = true;
  76 + break;
  77 + }
  78 + }
  79 + if(isSelectName && isSelectType) {
  80 + vm.theForm.$setDirty();
  81 + } else {
  82 + vm.theForm.$setPristine();
  83 + }
  84 + $timeout(function () {
  85 + vm.columnTypes.name.disable = isSelectName;
  86 + vm.columnTypes.type.disable = isSelectType;
  87 + if (angular.isDefined(vm.columnTypes.accessToken)) {
  88 + vm.columnTypes.accessToken.disable = isSelectCredentials;
  89 + }
  90 + });
  91 + }
  92 + }, true);
  93 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2019 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-table-container class="tb-table-select">
  19 + <table md-table>
  20 + <thead md-head>
  21 + <tr md-row>
  22 + <th md-column>&nbsp</th>
  23 + <th md-column>{{ 'import.column-example' | translate }}</th>
  24 + <th md-column>{{ 'import.column-type.column-type' | translate }}</th>
  25 + <th md-column>{{ 'import.column-key' | translate }}</th>
  26 + </tr>
  27 + </thead>
  28 + <tbody md-body>
  29 + <tr md-row ng-repeat="column in vm.columns track by $index">
  30 + <td md-cell>{{$index + 1}}</td>
  31 + <td md-cell>{{column.sampleData}}</td>
  32 + <td md-cell>
  33 + <md-select ng-model="column.type" required name="columnType"
  34 + aria-label="{{ 'import.column-type.column-type' | translate }}">
  35 + <md-option ng-repeat="type in vm.columnTypes" ng-value="type.value" ng-disabled="type.disable">
  36 + {{type.name | translate}}
  37 + </md-option>
  38 + </md-select>
  39 + </td>
  40 + <td md-cell>
  41 + <md-input-container md-no-float
  42 + ng-if="column.type != vm.columnTypes.name.value &&
  43 + column.type != vm.columnTypes.type.value &&
  44 + column.type != vm.columnTypes.accessToken.value">
  45 + <input required name="columnKeyName"
  46 + placeholder="{{ 'import.column-value' | translate }}"
  47 + ng-model="column.key"
  48 + aria-label="{{ 'import.column-value' | translate }}">
  49 + </md-input-container>
  50 + </td>
  51 + </tr>
  52 + </tbody>
  53 + </table>
  54 + <md-divider></md-divider>
  55 +</md-table-container>
@@ -48,7 +48,8 @@ @@ -48,7 +48,8 @@
48 "paste-reference": "Paste reference", 48 "paste-reference": "Paste reference",
49 "import": "Import", 49 "import": "Import",
50 "export": "Export", 50 "export": "Export",
51 - "share-via": "Share via {{provider}}" 51 + "share-via": "Share via {{provider}}",
  52 + "continue": "Continue"
52 }, 53 },
53 "aggregation": { 54 "aggregation": {
54 "aggregation": "Aggregation", 55 "aggregation": "Aggregation",
@@ -246,7 +247,9 @@ @@ -246,7 +247,9 @@
246 "select-asset": "Select asset", 247 "select-asset": "Select asset",
247 "no-assets-matching": "No assets matching '{{entity}}' were found.", 248 "no-assets-matching": "No assets matching '{{entity}}' were found.",
248 "asset-required": "Asset is required", 249 "asset-required": "Asset is required",
249 - "name-starts-with": "Asset name starts with" 250 + "name-starts-with": "Asset name starts with",
  251 + "import": "Import assets",
  252 + "asset-file": "Asset file"
250 }, 253 },
251 "attribute": { 254 "attribute": {
252 "attributes": "Attributes", 255 "attributes": "Attributes",
@@ -665,7 +668,9 @@ @@ -665,7 +668,9 @@
665 "is-gateway": "Is gateway", 668 "is-gateway": "Is gateway",
666 "public": "Public", 669 "public": "Public",
667 "device-public": "Device is public", 670 "device-public": "Device is public",
668 - "select-device": "Select device" 671 + "select-device": "Select device",
  672 + "import": "Import device",
  673 + "device-file": "Device file"
669 }, 674 },
670 "dialog": { 675 "dialog": {
671 "close": "Close dialog" 676 "close": "Close dialog"
@@ -1093,7 +1098,40 @@ @@ -1093,7 +1098,40 @@
1093 }, 1098 },
1094 "import": { 1099 "import": {
1095 "no-file": "No file selected", 1100 "no-file": "No file selected",
1096 - "drop-file": "Drop a JSON file or click to select a file to upload." 1101 + "drop-file": "Drop a JSON file or click to select a file to upload.",
  1102 + "drop-file-csv": "Drop a CSV file or click to select a file to upload.",
  1103 + "column-value": "Value",
  1104 + "column-title": "Title",
  1105 + "column-example": "Example value data",
  1106 + "column-key": "Attribute/telemetry key",
  1107 + "csv-delimiter": "CSV delimiter",
  1108 + "csv-first-line-header": "First line contains column names",
  1109 + "csv-update-data": "Update attributes/telemetry",
  1110 + "import-csv-number-columns-error": "A file should contain at least two columns",
  1111 + "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'",
  1112 + "column-type": {
  1113 + "name": "Name",
  1114 + "type": "Type",
  1115 + "column-type": "Column type",
  1116 + "client-attribute": "Client attribute",
  1117 + "shared-attribute": "Shared attribute",
  1118 + "server-attribute": "Server attribute",
  1119 + "timeseries": "Timeseries",
  1120 + "entity-field": "Entity field",
  1121 + "access-token": "Access token"
  1122 + },
  1123 + "stepper-text":{
  1124 + "select-file": "Select a file",
  1125 + "configuration": "Import configuration",
  1126 + "column-type": "Select columns type",
  1127 + "creat-entities": "Creating new entities",
  1128 + "done": "Done"
  1129 + },
  1130 + "message": {
  1131 + "create-entities": "{{count}} new entities were successfully created.",
  1132 + "update-entities": "{{count}} entities were successfully updated.",
  1133 + "error-entities": "There was an error creating {{count}} entities."
  1134 + }
1097 }, 1135 },
1098 "item": { 1136 "item": {
1099 "selected": "Selected" 1137 "selected": "Selected"