Browse Source

Fix conflicts

pull/1812/head
Igor Kulikov 7 years ago
parent
commit
eaddbbaf23
  1. 4
      ui/package-lock.json
  2. 1
      ui/package.json
  3. 15
      ui/src/app/api/asset.service.js
  4. 81
      ui/src/app/api/attribute.service.js
  5. 49
      ui/src/app/api/device.service.js
  6. 114
      ui/src/app/api/entity.service.js
  7. 3
      ui/src/app/app.js
  8. 28
      ui/src/app/asset/asset.controller.js
  9. 34
      ui/src/app/common/types.constant.js
  10. 30
      ui/src/app/device/device.controller.js
  11. 20
      ui/src/app/global-interceptor.service.js
  12. 1
      ui/src/app/help/help-links.constant.js
  13. 238
      ui/src/app/import-export/import-dialog-csv.controller.js
  14. 171
      ui/src/app/import-export/import-dialog-csv.tpl.html
  15. 20
      ui/src/app/import-export/import-dialog.scss
  16. 247
      ui/src/app/import-export/import-export.service.js
  17. 4
      ui/src/app/import-export/index.js
  18. 93
      ui/src/app/import-export/table-columns-assignment.directive.js
  19. 55
      ui/src/app/import-export/table-columns-assignment.tpl.html
  20. 46
      ui/src/app/locale/locale.constant-en_US.json

4
ui/package-lock.json

@ -8375,6 +8375,10 @@
"integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=", "integrity": "sha1-mnHEh0chjrylHlGmbaaCA4zct78=",
"dev": true "dev": true
}, },
"material-steppers": {
"version": "git://github.com/thingsboard/material-steppers.git#ee6241d63b97b7c4a3f3b240cdb347c02a3dcb0a",
"from": "git://github.com/thingsboard/material-steppers.git#master"
},
"material-ui": { "material-ui": {
"version": "0.16.7", "version": "0.16.7",
"resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.16.7.tgz", "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.16.7.tgz",

1
ui/package.json

@ -65,6 +65,7 @@
"leaflet": "^1.5.1", "leaflet": "^1.5.1",
"leaflet-polylinedecorator": "^1.6.0", "leaflet-polylinedecorator": "^1.6.0",
"leaflet-providers": "^1.8.0", "leaflet-providers": "^1.8.0",
"material-steppers": "git://github.com/thingsboard/material-steppers.git#master",
"material-ui": "^0.16.1", "material-ui": "^0.16.1",
"material-ui-number-input": "^5.0.16", "material-ui-number-input": "^5.0.16",
"md-color-picker": "0.2.6", "md-color-picker": "0.2.6",

15
ui/src/app/api/asset.service.js

@ -32,7 +32,8 @@ function AssetService($http, $q, customerService, userService) {
getCustomerAssets: getCustomerAssets, getCustomerAssets: getCustomerAssets,
findByQuery: findByQuery, findByQuery: findByQuery,
fetchAssetsByNameFilter: fetchAssetsByNameFilter, fetchAssetsByNameFilter: fetchAssetsByNameFilter,
getAssetTypes: getAssetTypes getAssetTypes: getAssetTypes,
findByName: findByName
} }
return service; return service;
@ -276,4 +277,16 @@ function AssetService($http, $q, customerService, userService) {
return deferred.promise; 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;
}
} }

81
ui/src/app/api/attribute.service.js

@ -30,7 +30,9 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
subscribeForEntityAttributes: subscribeForEntityAttributes, subscribeForEntityAttributes: subscribeForEntityAttributes,
unsubscribeForEntityAttributes: unsubscribeForEntityAttributes, unsubscribeForEntityAttributes: unsubscribeForEntityAttributes,
saveEntityAttributes: saveEntityAttributes, saveEntityAttributes: saveEntityAttributes,
deleteEntityAttributes: deleteEntityAttributes deleteEntityAttributes: deleteEntityAttributes,
saveEntityTimeseries: saveEntityTimeseries,
deleteEntityTimeseries: deleteEntityTimeseries
} }
return service; 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 deferred = $q.defer();
var attributesData = {}; var attributesData = {};
var deleteAttributes = []; var deleteAttributes = [];
@ -229,7 +232,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
} }
if (Object.keys(attributesData).length) { if (Object.keys(attributesData).length) {
var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope; 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) { if (deleteEntityAttributesPromise) {
deleteEntityAttributesPromise.then( deleteEntityAttributesPromise.then(
function success() { function success() {
@ -260,7 +263,57 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
return deferred.promise; 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<timeseries.length;a++) {
if (angular.isDefined(timeseries[a].value) && timeseries[a].value !== null) {
timeseriesData[timeseries[a].key] = timeseries[a].value;
} else {
deleteTimeseries.push(timeseries[a]);
}
}
var deleteEntityTimeseriesPromise;
if (deleteTimeseries.length) {
deleteEntityTimeseriesPromise = deleteEntityTimeseries(entityType, entityId, deleteTimeseries, config);
}
if (Object.keys(timeseriesData).length) {
var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/timeseries/' + timeseriesScope;
$http.post(url, timeseriesData, config).then(function success(response) {
if (deleteEntityTimeseriesPromise) {
deleteEntityTimeseriesPromise.then(
function success() {
deferred.resolve(response.data);
},
function fail() {
deferred.reject();
}
)
} else {
deferred.resolve(response.data);
}
}, function fail() {
deferred.reject();
});
} else if (deleteEntityTimeseriesPromise) {
deleteEntityTimeseriesPromise.then(
function success() {
deferred.resolve();
},
function fail() {
deferred.reject();
}
)
} else {
deferred.resolve();
}
return deferred.promise;
}
function deleteEntityAttributes(entityType, entityId, attributeScope, attributes, config) {
config = config || {};
var deferred = $q.defer(); var deferred = $q.defer();
var keys = ''; var keys = '';
for (var i = 0; i < attributes.length; i++) { for (var i = 0; i < attributes.length; i++) {
@ -270,7 +323,7 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
keys += attributes[i].key; keys += attributes[i].key;
} }
var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys; var url = '/api/plugins/telemetry/' + entityType + '/' + entityId + '/' + attributeScope + '?keys=' + keys;
$http.delete(url).then(function success() { $http.delete(url, config).then(function success() {
deferred.resolve(); deferred.resolve();
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -278,5 +331,23 @@ function AttributeService($http, $q, $filter, types, telemetryWebsocketService)
return deferred.promise; return deferred.promise;
} }
function deleteEntityTimeseries(entityType, entityId, timeseries, config) {
config = config || {};
var deferred = $q.defer();
var keys = '';
for (var i = 0; i < timeseries.length; i++) {
if (i > 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;
}
} }

49
ui/src/app/api/device.service.js

@ -42,8 +42,9 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
sendOneWayRpcCommand: sendOneWayRpcCommand, sendOneWayRpcCommand: sendOneWayRpcCommand,
sendTwoWayRpcCommand: sendTwoWayRpcCommand, sendTwoWayRpcCommand: sendTwoWayRpcCommand,
findByQuery: findByQuery, findByQuery: findByQuery,
getDeviceTypes: getDeviceTypes getDeviceTypes: getDeviceTypes,
} findByName: findByName
};
return service; return service;
@ -124,7 +125,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
if (!config) { if (!config) {
config = {}; config = {};
} }
config = Object.assign(config, { ignoreErrors: ignoreErrors }); config = Object.assign(config, {ignoreErrors: ignoreErrors});
$http.get(url, config).then(function success(response) { $http.get(url, config).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail(response) { }, function fail(response) {
@ -136,8 +137,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
function getDevices(deviceIds, config) { function getDevices(deviceIds, config) {
var deferred = $q.defer(); var deferred = $q.defer();
var ids = ''; var ids = '';
for (var i=0;i<deviceIds.length;i++) { for (var i = 0; i < deviceIds.length; i++) {
if (i>0) { if (i > 0) {
ids += ','; ids += ',';
} }
ids += deviceIds[i]; ids += deviceIds[i];
@ -146,11 +147,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
$http.get(url, config).then(function success(response) { $http.get(url, config).then(function success(response) {
var devices = response.data; var devices = response.data;
devices.sort(function (device1, device2) { devices.sort(function (device1, device2) {
var id1 = device1.id.id; var id1 = device1.id.id;
var id2 = device2.id.id; var id2 = device2.id.id;
var index1 = deviceIds.indexOf(id1); var index1 = deviceIds.indexOf(id1);
var index2 = deviceIds.indexOf(id2); var index2 = deviceIds.indexOf(id2);
return index1 - index2; return index1 - index2;
}); });
deferred.resolve(devices); deferred.resolve(devices);
}, function fail(response) { }, function fail(response) {
@ -159,10 +160,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
return deferred.promise; return deferred.promise;
} }
function saveDevice(device) { function saveDevice(device, config) {
config = config || {};
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device'; 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); deferred.resolve(response.data);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -181,7 +183,8 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
return deferred.promise; return deferred.promise;
} }
function getDeviceCredentials(deviceId, sync) { function getDeviceCredentials(deviceId, sync, config) {
config = config || {};
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device/' + deviceId + '/credentials'; var url = '/api/device/' + deviceId + '/credentials';
if (sync) { if (sync) {
@ -196,7 +199,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
deferred.reject(); deferred.reject();
} }
} else { } else {
$http.get(url, null).then(function success(response) { $http.get(url, config).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -205,10 +208,11 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
return deferred.promise; return deferred.promise;
} }
function saveDeviceCredentials(deviceCredentials) { function saveDeviceCredentials(deviceCredentials, config) {
config = config || {};
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device/credentials'; 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); deferred.resolve(response.data);
}, function fail() { }, function fail() {
deferred.reject(); deferred.reject();
@ -297,7 +301,7 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
if (!config) { if (!config) {
config = {}; config = {};
} }
config = Object.assign(config, { ignoreErrors: ignoreErrors }); config = Object.assign(config, {ignoreErrors: ignoreErrors});
$http.post(url, query, config).then(function success(response) { $http.post(url, query, config).then(function success(response) {
deferred.resolve(response.data); deferred.resolve(response.data);
}, function fail() { }, function fail() {
@ -317,4 +321,15 @@ function DeviceService($http, $q, $window, userService, attributeService, custom
return deferred.promise; 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;
}
} }

114
ui/src/app/api/entity.service.js

@ -38,6 +38,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
createAlarmSourceFromSubscriptionInfo: createAlarmSourceFromSubscriptionInfo, createAlarmSourceFromSubscriptionInfo: createAlarmSourceFromSubscriptionInfo,
getRelatedEntities: getRelatedEntities, getRelatedEntities: getRelatedEntities,
saveRelatedEntity: saveRelatedEntity, saveRelatedEntity: saveRelatedEntity,
saveEntityParameters: saveEntityParameters,
getRelatedEntity: getRelatedEntity, getRelatedEntity: getRelatedEntity,
deleteRelatedEntity: deleteRelatedEntity, deleteRelatedEntity: deleteRelatedEntity,
moveEntity: moveEntity, moveEntity: moveEntity,
@ -1072,6 +1073,119 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
return deferred.promise; 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) { function getRelatedEntity(entityId, keys, typeTranslatePrefix) {
var deferred = $q.defer(); var deferred = $q.defer();
getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then( getEntityPromise(entityId.entityType, entityId.id, {ignoreLoading: true}).then(

3
ui/src/app/app.js

@ -54,6 +54,8 @@ import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min'; import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import 'ngFlowchart/dist/ngFlowchart'; import 'ngFlowchart/dist/ngFlowchart';
import 'jstree/dist/jstree.min'; 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 'jstree-bootstrap-theme/dist/themes/proton/style.min.css';
import 'typeface-roboto'; import 'typeface-roboto';
import 'font-awesome/css/font-awesome.min.css'; import 'font-awesome/css/font-awesome.min.css';
@ -127,6 +129,7 @@ angular.module('thingsboard', [
react.name, react.name,
'flow', 'flow',
'flowchart', 'flowchart',
'mdSteppers',
thingsboardThirdpartyFix, thingsboardThirdpartyFix,
thingsboardTranslateHandler, thingsboardTranslateHandler,
thingsboardLogin, thingsboardLogin,

28
ui/src/app/asset/asset.controller.js

@ -48,7 +48,7 @@ export function AssetCardController(types) {
/*@ngInject*/ /*@ngInject*/
export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams, export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams,
$document, $mdDialog, $q, $translate, types) { $document, $mdDialog, $q, $translate, types, importExport) {
var customerId = $stateParams.customerId; var customerId = $stateParams.customerId;
@ -56,6 +56,29 @@ export function AssetController($rootScope, userService, assetService, customerS
var assetGroupActionsList = []; 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; var vm = this;
vm.types = types; vm.types = types;
@ -77,6 +100,7 @@ export function AssetController($rootScope, userService, assetService, customerS
actionsList: assetActionsList, actionsList: assetActionsList,
groupActionsList: assetGroupActionsList, groupActionsList: assetGroupActionsList,
addItemActions: assetAddItemActionsList,
onGridInited: gridInited, onGridInited: gridInited,
@ -294,6 +318,8 @@ export function AssetController($rootScope, userService, assetService, customerS
} else if (vm.assetsScope === 'customer_user') { } else if (vm.assetsScope === 'customer_user') {
vm.assetGridConfig.addItemAction = {}; vm.assetGridConfig.addItemAction = {};
} }
vm.assetGridConfig.addItemActions = [];
} }
vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction; vm.assetGridConfig.refreshParamsFunc = refreshAssetsParamsFunction;

34
ui/src/app/common/types.constant.js

@ -350,6 +350,40 @@ export default angular.module('thingsboard.types', [])
rulenode: "RULE_NODE", rulenode: "RULE_NODE",
entityView: "ENTITY_VIEW" 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: { aliasEntityType: {
current_customer: "CURRENT_CUSTOMER" current_customer: "CURRENT_CUSTOMER"
}, },

30
ui/src/app/device/device.controller.js

@ -49,7 +49,7 @@ export function DeviceCardController(types) {
/*@ngInject*/ /*@ngInject*/
export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams, export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams,
$document, $mdDialog, $q, $translate, types) { $document, $mdDialog, $q, $translate, types, importExport) {
var customerId = $stateParams.customerId; var customerId = $stateParams.customerId;
@ -57,6 +57,29 @@ export function DeviceController($rootScope, userService, deviceService, custome
var deviceGroupActionsList = []; 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; var vm = this;
vm.types = types; vm.types = types;
@ -78,12 +101,12 @@ export function DeviceController($rootScope, userService, deviceService, custome
actionsList: deviceActionsList, actionsList: deviceActionsList,
groupActionsList: deviceGroupActionsList, groupActionsList: deviceGroupActionsList,
addItemActions: deviceAddItemActionsList,
onGridInited: gridInited, onGridInited: gridInited,
addItemTemplateUrl: addDeviceTemplate, addItemTemplateUrl: addDeviceTemplate,
addItemText: function() { return $translate.instant('device.add-device-text') },
noItemsText: function() { return $translate.instant('device.no-devices-text') }, noItemsText: function() { return $translate.instant('device.no-devices-text') },
itemDetailsText: function() { return $translate.instant('device.device-details') }, itemDetailsText: function() { return $translate.instant('device.device-details') },
isDetailsReadOnly: isCustomerUser, isDetailsReadOnly: isCustomerUser,
@ -314,7 +337,6 @@ export function DeviceController($rootScope, userService, deviceService, custome
icon: "add" icon: "add"
}; };
} else if (vm.devicesScope === 'customer_user') { } else if (vm.devicesScope === 'customer_user') {
deviceActionsList.push( deviceActionsList.push(
{ {
@ -329,6 +351,8 @@ export function DeviceController($rootScope, userService, deviceService, custome
vm.deviceGridConfig.addItemAction = {}; vm.deviceGridConfig.addItemAction = {};
} }
vm.deviceGridConfig.addItemActions = [];
} }
vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction; vm.deviceGridConfig.refreshParamsFunc = refreshDevicesParamsFunction;

20
ui/src/app/global-interceptor.service.js

@ -21,6 +21,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
var userService; var userService;
var types; var types;
var http; var http;
var timeout;
var internalUrlPrefixes = [ var internalUrlPrefixes = [
'/api/auth/token', '/api/auth/token',
@ -71,6 +72,13 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
return http; return http;
} }
function getTimeout() {
if (!timeout) {
timeout = $injector.get("$timeout");
}
return timeout;
}
function rejectionErrorCode(rejection) { function rejectionErrorCode(rejection) {
if (rejection && rejection.data && rejection.data.errorCode) { if (rejection && rejection.data && rejection.data.errorCode) {
return rejection.data.errorCode; return rejection.data.errorCode;
@ -144,12 +152,20 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
return response; return response;
} }
function retryRequest (httpConfig) {
var thisTimeout = 1000 + Math.random() * 3000;
return getTimeout()(function() {
return getHttp()(httpConfig);
}, thisTimeout);
}
function responseError(rejection) { function responseError(rejection) {
if (rejection.config.url.startsWith('/api/')) { if (rejection.config.url.startsWith('/api/')) {
updateLoadingState(rejection.config, false); updateLoadingState(rejection.config, false);
} }
var unhandled = false; var unhandled = false;
var ignoreErrors = rejection.config.ignoreErrors; var ignoreErrors = rejection.config.ignoreErrors;
var resendRequest = rejection.config.resendRequest;
if (rejection.refreshTokenPending || rejection.status === 401) { if (rejection.refreshTokenPending || rejection.status === 401) {
var errorCode = rejectionErrorCode(rejection); var errorCode = rejectionErrorCode(rejection);
if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
@ -161,6 +177,10 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
if (!ignoreErrors) { if (!ignoreErrors) {
$rootScope.$broadcast('forbidden'); $rootScope.$broadcast('forbidden');
} }
} else if (rejection.status === 429) {
if (resendRequest) {
return retryRequest(rejection.config);
}
} else if (rejection.status === 0 || rejection.status === -1) { } else if (rejection.status === 0 || rejection.status === -1) {
getToast().showError(getTranslate().instant('error.unable-to-connect')); getToast().showError(getTranslate().instant('error.unable-to-connect'));
} else if (!rejection.config.url.startsWith('/api/plugins/rpc')) { } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {

1
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", assets: helpBaseUrl + "/docs/user-guide/ui/assets",
devices: helpBaseUrl + "/docs/user-guide/ui/devices", devices: helpBaseUrl + "/docs/user-guide/ui/devices",
entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views", entityViews: helpBaseUrl + "/docs/user-guide/ui/entity-views",
entitiesImport: helpBaseUrl + "/docs/user-guide/bulk-provisioning",
dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards", dashboards: helpBaseUrl + "/docs/user-guide/ui/dashboards",
users: helpBaseUrl + "/docs/user-guide/ui/users", users: helpBaseUrl + "/docs/user-guide/ui/users",
widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles", widgetsBundles: helpBaseUrl + "/docs/user-guide/ui/widget-library#bundles",

238
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();
}
}
}

171
ui/src/app/import-export/import-dialog-csv.tpl.html

@ -0,0 +1,171 @@
<!--
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.
-->
<md-dialog aria-label="{{ vm.importTitle | translate }}" class="tb-import-stepper" tb-help="'entitiesImport'"
help-container-id="help-container">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>{{ vm.importTitle }}</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()" ng-disabled="vm.isImportData">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<span style="max-height: 5px; height: 5px;" flex></span>
<md-dialog-content>
<md-stepper id="import-stepper" md-mobile-step-text="vm.isMobileStepText" md-vertical="vm.isVertical"
md-linear="vm.isLinear" md-alternative="vm.isAlternative">
<md-step md-label="{{ 'import.stepper-text.select-file' | translate }}">
<md-step-body>
<form name="vm.theFormStep1">
<fieldset ng-disabled="$root.loading">
<div layout="column" layout-padding>
<div class="tb-container">
<label class="tb-label" translate>{{ vm.importFileLabel }}</label>
<div flow-init="{singleFile:true}"
flow-file-added="vm.fileAdded( $file )" class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click="vm.clearFile()"
class="tb-file-clear-btn md-icon-button md-primary"
aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}"
class="material-icons">
close
</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="select" translate>import.drop-file-csv</label>
<input class="file-input" flow-btn
flow-attrs="{accept:'.csv,application/csv,text/csv'}"
id="select">
</div>
</div>
</div>
<div>
<div ng-show="!vm.fileName" translate>import.no-file</div>
<div ng-show="vm.fileName">{{ vm.fileName }}</div>
</div>
</div>
</fieldset>
</form>
</md-step-body>
<md-step-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()">
{{ 'action.cancel' | translate }}
</md-button>
<md-button class="md-primary md-raised"
ng-disabled="$root.loading || !vm.theFormStep1.$dirty || !vm.theFormStep1.$valid || !vm.importData"
ng-click="vm.nextStep(2);">
{{ 'action.continue' | translate }}
</md-button>
</md-step-actions>
</md-step>
<md-step md-label="{{ 'import.stepper-text.configuration' | translate }}">
<md-step-body>
<div layout="column">
<md-input-container>
<label translate>import.csv-delimiter</label>
<md-select ng-model="vm.importParameters.delim">
<md-option ng-repeat="delimiter in vm.delimiters" ng-value="delimiter.key">
{{delimiter.value}}
</md-option>
</md-select>
</md-input-container>
<md-checkbox ng-model="vm.importParameters.isHeader"
aria-label="First line contains column names">
{{ 'import.csv-first-line-header' | translate }}
</md-checkbox>
<md-checkbox ng-model="vm.importParameters.isUpdate" aria-label="Update attributes/telemetry">
{{ 'import.csv-update-data' | translate }}
</md-checkbox>
</div>
</md-step-body>
<md-step-actions layout="row">
<md-button ng-disabled="$root.loading" ng-click="vm.previousStep(1);">Back
</md-button>
<span flex></span>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()">
{{ 'action.cancel' | translate }}
</md-button>
<md-button class="md-primary md-raised" ng-disabled="$root.loading" ng-click="vm.nextStep(3);">
{{ 'action.continue' | translate }}
</md-button>
</md-step-actions>
</md-step>
<md-step md-label="{{ 'import.stepper-text.column-type' | translate }}">
<md-step-body>
<form name="vm.theFormStep3">
<tb-table-columns-assignment columns="vm.columnsParam" the-form="vm.theFormStep3"
entity-type="vm.entityType"></tb-table-columns-assignment>
</form>
</md-step-body>
<md-step-actions layout="row">
<md-button ng-disabled="$root.loading" ng-click="vm.previousStep();">Back
</md-button>
<span flex></span>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()">
{{ 'action.cancel' | translate }}
</md-button>
<md-button class="md-primary md-raised"
ng-disabled="$root.loading || !vm.theFormStep3.$dirty || !vm.theFormStep3.$valid"
ng-click="vm.nextStep(4);">
{{ 'action.continue' | translate }}
</md-button>
</md-step-actions>
</md-step>
<md-step md-label="{{ 'import.stepper-text.creat-entities' | translate }}">
<md-step-body>
<md-progress-linear class="md-warn tb-import-progress" md-mode="determinate"
value="{{vm.progressCreate}}"></md-progress-linear>
</md-step-body>
</md-step>
<md-step md-label="{{ 'import.stepper-text.done' | translate }}">
<md-step-body layout="column">
<div>
<p class="md-body-1" translate translate-values="{count: vm.statistical.create.entity}"
ng-if="vm.statistical.create && vm.statistical.create.entity">import.message.create-entities</p>
<p class="md-body-1" translate translate-values="{count: vm.statistical.update.entity}"
ng-if="vm.statistical.update && vm.statistical.update.entity">import.message.update-entities</p>
<p class="md-body-1" translate translate-values="{count: vm.statistical.error.entity}"
ng-if="vm.statistical.error && vm.statistical.error.entity">import.message.error-entities</p>
</div>
</md-step-body>
<md-step-actions layout="row">
<span flex></span>
<md-button class="md-primary md-raised" ng-disabled="$root.loading" ng-click="vm.nextStep(6);">
{{ 'action.ok' | translate }}
</md-button>
</md-step-actions>
</md-step>
</md-stepper>
</md-dialog-content>
</md-dialog>

20
ui/src/app/import-export/import-dialog.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../../scss/constants.scss";
$previewSize: 100px !default; $previewSize: 100px !default;
.tb-file-select-container { .tb-file-select-container {
@ -40,3 +42,21 @@ $previewSize: 100px !default;
top: 50%; top: 50%;
transform: translate(0%, -50%) !important; 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;
}

247
ui/src/app/import-export/import-export.service.js

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * 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 importDialogTemplate from './import-dialog.tpl.html';
import importDialogCSVTemplate from './import-dialog-csv.tpl.html';
import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html'; import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* 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 */ /* eslint-disable no-undef, angular/window-service, angular/document-service */
/*@ngInject*/ /*@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) { dashboardUtils, entityService, dashboardService, ruleChainService, widgetService, toast, attributeService) {
@ -41,7 +42,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
importWidgetsBundle: importWidgetsBundle, importWidgetsBundle: importWidgetsBundle,
exportExtension: exportExtension, exportExtension: exportExtension,
importExtension: importExtension, importExtension: importExtension,
exportToPc: exportToPc importEntities: importEntities,
convertCSVToJson: convertCSVToJson,
exportToPc: exportToPc,
createMultiEntity: createMultiEntity
}; };
return service; return service;
@ -54,11 +58,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
var bundleAlias = widgetsBundle.alias; var bundleAlias = widgetsBundle.alias;
var isSystem = widgetsBundle.tenantId.id === types.id.nullUid; var isSystem = widgetsBundle.tenantId.id === types.id.nullUid;
widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then( widgetService.getBundleWidgetTypes(bundleAlias, isSystem).then(
function success (widgetTypes) { function success(widgetTypes) {
prepareExport(widgetsBundle); prepareExport(widgetsBundle);
var widgetsBundleItem = { var widgetsBundleItem = {
widgetsBundle: prepareExport(widgetsBundle), widgetsBundle: prepareExport(widgetsBundle),
widgetTypes: [] widgetTypes: []
}; };
for (var t in widgetTypes) { for (var t in widgetTypes) {
var widgetType = widgetTypes[t]; var widgetType = widgetTypes[t];
@ -68,10 +72,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
widgetsBundleItem.widgetTypes.push(prepareExport(widgetType)); widgetsBundleItem.widgetTypes.push(prepareExport(widgetType));
} }
var name = widgetsBundle.title; var name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g, "_");
exportToPc(widgetsBundleItem, name + '.json'); exportToPc(widgetsBundleItem, name + '.json');
}, },
function fail (rejection) { function fail(rejection) {
var message = rejection; var message = rejection;
if (!message) { if (!message) {
message = $translate.instant('error.unknown-error'); message = $translate.instant('error.unknown-error');
@ -168,7 +172,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
delete widgetType.bundleAlias; delete widgetType.bundleAlias;
} }
var name = widgetType.name; var name = widgetType.name;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g, "_");
exportToPc(prepareExport(widgetType), name + '.json'); exportToPc(prepareExport(widgetType), name + '.json');
}, },
function fail(rejection) { function fail(rejection) {
@ -209,8 +213,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function validateImportedWidgetType(widgetType) { function validateImportedWidgetType(widgetType) {
if (angular.isUndefined(widgetType.name) if (angular.isUndefined(widgetType.name)
|| angular.isUndefined(widgetType.descriptor)) || angular.isUndefined(widgetType.descriptor)) {
{
return false; return false;
} }
return true; return true;
@ -228,7 +231,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
metadata: prepareRuleChainMetaData(ruleChainMetaData) metadata: prepareRuleChainMetaData(ruleChainMetaData)
}; };
var name = ruleChain.name; var name = ruleChain.name;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g, "_");
exportToPc(ruleChainExport, name + '.json'); exportToPc(ruleChainExport, name + '.json');
}, },
(rejection) => { (rejection) => {
@ -253,7 +256,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function prepareRuleChainMetaData(ruleChainMetaData) { function prepareRuleChainMetaData(ruleChainMetaData) {
delete ruleChainMetaData.ruleChainId; delete ruleChainMetaData.ruleChainId;
for (var i=0;i<ruleChainMetaData.nodes.length;i++) { for (var i = 0; i < ruleChainMetaData.nodes.length; i++) {
var node = ruleChainMetaData.nodes[i]; var node = ruleChainMetaData.nodes[i];
delete node.ruleChainId; delete node.ruleChainId;
ruleChainMetaData.nodes[i] = prepareExport(node); ruleChainMetaData.nodes[i] = prepareExport(node);
@ -305,7 +308,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function exportWidget(dashboard, sourceState, sourceLayout, widget) { function exportWidget(dashboard, sourceState, sourceLayout, widget) {
var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); var widgetItem = itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
var name = widgetItem.widget.config.title; var name = widgetItem.widget.config.title;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g, "_");
exportToPc(prepareExport(widgetItem), name + '.json'); exportToPc(prepareExport(widgetItem), name + '.json');
} }
@ -415,10 +418,10 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
var aliasIds = Object.keys(entityAliases); var aliasIds = Object.keys(entityAliases);
if (aliasIds.length > 0) { if (aliasIds.length > 0) {
processEntityAliases(entityAliases, aliasIds).then( processEntityAliases(entityAliases, aliasIds).then(
function(missingEntityAliases) { function (missingEntityAliases) {
if (Object.keys(missingEntityAliases).length > 0) { if (Object.keys(missingEntityAliases).length > 0) {
editMissingAliases($event, [ widget ], editMissingAliases($event, [widget],
true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then( true, 'dashboard.widget-import-missing-aliases-title', missingEntityAliases).then(
function success(updatedEntityAliases) { function success(updatedEntityAliases) {
for (var aliasId in updatedEntityAliases) { for (var aliasId in updatedEntityAliases) {
var entityAlias = updatedEntityAliases[aliasId]; var entityAlias = updatedEntityAliases[aliasId];
@ -483,14 +486,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function success(targetLayout) { function success(targetLayout) {
itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget, itembuffer.addWidgetToDashboard(dashboard, targetState, targetLayout, widget,
aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then( aliasesInfo, onAliasesUpdateFunction, originalColumns, originalSize, -1, -1).then(
function() { function () {
deferred.resolve( deferred.resolve(
{ {
widget: widget, widget: widget,
layoutId: targetLayout layoutId: targetLayout
} }
); );
} }
); );
}, },
function fail() { function fail() {
@ -505,7 +508,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
dashboardService.getDashboard(dashboardId).then( dashboardService.getDashboard(dashboardId).then(
function success(dashboard) { function success(dashboard) {
var name = dashboard.title; var name = dashboard.title;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g, "_");
exportToPc(prepareDashboardExport(dashboard), name + '.json'); exportToPc(prepareDashboardExport(dashboard), name + '.json');
}, },
function fail(rejection) { function fail(rejection) {
@ -538,13 +541,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard); dashboard = dashboardUtils.validateAndUpdateDashboard(dashboard);
var entityAliases = dashboard.configuration.entityAliases; var entityAliases = dashboard.configuration.entityAliases;
if (entityAliases) { if (entityAliases) {
var aliasIds = Object.keys( entityAliases ); var aliasIds = Object.keys(entityAliases);
if (aliasIds.length > 0) { if (aliasIds.length > 0) {
processEntityAliases(entityAliases, aliasIds).then( processEntityAliases(entityAliases, aliasIds).then(
function(missingEntityAliases) { function (missingEntityAliases) {
if (Object.keys( missingEntityAliases ).length > 0) { if (Object.keys(missingEntityAliases).length > 0) {
editMissingAliases($event, dashboard.configuration.widgets, 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) { function success(updatedEntityAliases) {
for (var aliasId in updatedEntityAliases) { for (var aliasId in updatedEntityAliases) {
entityAliases[aliasId] = updatedEntityAliases[aliasId]; entityAliases[aliasId] = updatedEntityAliases[aliasId];
@ -575,6 +578,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return deferred.promise; 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) { function saveImportedDashboard(dashboard, deferred) {
dashboardService.saveDashboard(dashboard).then( dashboardService.saveDashboard(dashboard).then(
function success() { function success() {
@ -594,14 +625,13 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
} }
function exportExtension(extensionId) { function exportExtension(extensionId) {
getExtension(extensionId) getExtension(extensionId)
.then( .then(
function success(extension) { function success(extension) {
var name = extension.title; var name = extension.title;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g, "_");
exportToPc(prepareExport(extension), name + '.json'); exportToPc(prepareExport(extension), name + '.json');
}, },
function fail(rejection) { function fail(rejection) {
@ -661,7 +691,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function validateImportedExtension(configuration) { function validateImportedExtension(configuration) {
if (configuration.length) { if (configuration.length) {
for (let i = 0; i < configuration.length; i++) { 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; return false;
} }
} }
@ -692,7 +722,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
var aliasId = aliasIds[index]; var aliasId = aliasIds[index];
var entityAlias = entityAliases[aliasId]; var entityAlias = entityAliases[aliasId];
entityService.checkEntityAlias(entityAlias).then( entityService.checkEntityAlias(entityAlias).then(
function(result) { function (result) {
if (result) { if (result) {
checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred); checkNextEntityAliasOrComplete(index, aliasIds, entityAliases, missingEntityAliases, deferred);
} else { } else {
@ -732,6 +762,124 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return deferred.promise; 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 // Common functions
function prepareExport(data) { function prepareExport(data) {
@ -771,8 +919,7 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
if (window.navigator && window.navigator.msSaveOrOpenBlob) { if (window.navigator && window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(blob, filename); window.navigator.msSaveOrOpenBlob(blob, filename);
} } else {
else{
var e = document.createEvent('MouseEvents'), var e = document.createEvent('MouseEvents'),
a = document.createElement('a'); a = document.createElement('a');
@ -807,6 +954,34 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return deferred.promise; 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 */ /* eslint-enable no-undef, angular/window-service, angular/document-service */

4
ui/src/app/import-export/index.js

@ -15,9 +15,13 @@
*/ */
import ImportExport from './import-export.service'; import ImportExport from './import-export.service';
import ImportDialogController from './import-dialog.controller'; 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', []) export default angular.module('thingsboard.importexport', [])
.factory('importExport', ImportExport) .factory('importExport', ImportExport)
.controller('ImportDialogController', ImportDialogController) .controller('ImportDialogController', ImportDialogController)
.controller('ImportDialogCSVController', ImportDialogCSVController)
.directive('tbTableColumnsAssignment', TableColumnsAssignment)
.name; .name;

93
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);
}

55
ui/src/app/import-export/table-columns-assignment.tpl.html

@ -0,0 +1,55 @@
<!--
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.
-->
<md-table-container class="tb-table-select">
<table md-table>
<thead md-head>
<tr md-row>
<th md-column>&nbsp</th>
<th md-column>{{ 'import.column-example' | translate }}</th>
<th md-column>{{ 'import.column-type.column-type' | translate }}</th>
<th md-column>{{ 'import.column-key' | translate }}</th>
</tr>
</thead>
<tbody md-body>
<tr md-row ng-repeat="column in vm.columns track by $index">
<td md-cell>{{$index + 1}}</td>
<td md-cell>{{column.sampleData}}</td>
<td md-cell>
<md-select ng-model="column.type" required name="columnType"
aria-label="{{ 'import.column-type.column-type' | translate }}">
<md-option ng-repeat="type in vm.columnTypes" ng-value="type.value" ng-disabled="type.disable">
{{type.name | translate}}
</md-option>
</md-select>
</td>
<td md-cell>
<md-input-container md-no-float
ng-if="column.type != vm.columnTypes.name.value &&
column.type != vm.columnTypes.type.value &&
column.type != vm.columnTypes.accessToken.value">
<input required name="columnKeyName"
placeholder="{{ 'import.column-value' | translate }}"
ng-model="column.key"
aria-label="{{ 'import.column-value' | translate }}">
</md-input-container>
</td>
</tr>
</tbody>
</table>
<md-divider></md-divider>
</md-table-container>

46
ui/src/app/locale/locale.constant-en_US.json

@ -48,7 +48,8 @@
"paste-reference": "Paste reference", "paste-reference": "Paste reference",
"import": "Import", "import": "Import",
"export": "Export", "export": "Export",
"share-via": "Share via {{provider}}" "share-via": "Share via {{provider}}",
"continue": "Continue"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -246,7 +247,9 @@
"select-asset": "Select asset", "select-asset": "Select asset",
"no-assets-matching": "No assets matching '{{entity}}' were found.", "no-assets-matching": "No assets matching '{{entity}}' were found.",
"asset-required": "Asset is required", "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": { "attribute": {
"attributes": "Attributes", "attributes": "Attributes",
@ -665,7 +668,9 @@
"is-gateway": "Is gateway", "is-gateway": "Is gateway",
"public": "Public", "public": "Public",
"device-public": "Device is public", "device-public": "Device is public",
"select-device": "Select device" "select-device": "Select device",
"import": "Import device",
"device-file": "Device file"
}, },
"dialog": { "dialog": {
"close": "Close dialog" "close": "Close dialog"
@ -1093,7 +1098,40 @@
}, },
"import": { "import": {
"no-file": "No file selected", "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": { "item": {
"selected": "Selected" "selected": "Selected"

Loading…
Cancel
Save