Commit 41e572490ef373f6d2718f1fe8e8f10b7e986b88

Authored by Viacheslav Klimov
2 parents 33592ca0 e3bf26e2

Merge branch 'feature/bulk-import/device-credentials' of https://github.com/vvll…

…add28/thingsboard into feature/bulk-import
... ... @@ -53,7 +53,7 @@ import {
53 53 ImportEntityData
54 54 } from '@shared/models/entity.models';
55 55 import { EntityRelationService } from '@core/http/entity-relation.service';
56   -import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull } from '@core/utils';
  56 +import { deepClone, generateSecret, guid, isDefined, isDefinedAndNotNull, isNotEmptyStr } from '@core/utils';
57 57 import { Asset } from '@shared/models/asset.models';
58 58 import { Device, DeviceCredentialsType } from '@shared/models/device.models';
59 59 import { AttributeService } from '@core/http/attribute.service';
... ... @@ -954,7 +954,12 @@ export class EntityService {
954 954 map(() => {
955 955 return { create: { entity: 1 } } as ImportEntitiesResultInfo;
956 956 }),
957   - catchError(err => of({ error: { entity: 1 } } as ImportEntitiesResultInfo))
  957 + catchError(err => of({
  958 + error: {
  959 + entity: 1,
  960 + errors: err.message
  961 + }
  962 + } as ImportEntitiesResultInfo))
958 963 );
959 964 }),
960 965 catchError(err => {
... ... @@ -978,13 +983,28 @@ export class EntityService {
978 983 map(() => {
979 984 return { update: { entity: 1 } } as ImportEntitiesResultInfo;
980 985 }),
981   - catchError(updateError => of({ error: { entity: 1 } } as ImportEntitiesResultInfo))
  986 + catchError(updateError => of({
  987 + error: {
  988 + entity: 1,
  989 + errors: updateError.message
  990 + }
  991 + } as ImportEntitiesResultInfo))
982 992 );
983 993 }),
984   - catchError(findErr => of({ error: { entity: 1 } } as ImportEntitiesResultInfo))
  994 + catchError(findErr => of({
  995 + error: {
  996 + entity: 1,
  997 + errors: `Line: ${entityData.lineNumber}; Error: ${findErr.error.message}`
  998 + }
  999 + } as ImportEntitiesResultInfo))
985 1000 );
986 1001 } else {
987   - return of({ error: { entity: 1 } } as ImportEntitiesResultInfo);
  1002 + return of({
  1003 + error: {
  1004 + entity: 1,
  1005 + errors: `Line: ${entityData.lineNumber}; Error: ${err.error.message}`
  1006 + }
  1007 + } as ImportEntitiesResultInfo);
988 1008 }
989 1009 })
990 1010 );
... ... @@ -1040,7 +1060,6 @@ export class EntityService {
1040 1060 break;
1041 1061 }
1042 1062 return saveEntityObservable;
1043   -
1044 1063 }
1045 1064
1046 1065 private getUpdateEntityTasks(entityType: EntityType, entityData: ImportEntityData | EdgeImportEntityData,
... ... @@ -1113,15 +1132,31 @@ export class EntityService {
1113 1132 public saveEntityData(entityId: EntityId, entityData: ImportEntityData, config?: RequestConfig): Observable<any> {
1114 1133 const observables: Observable<string>[] = [];
1115 1134 let observable: Observable<string>;
1116   - if (entityData.accessToken && entityData.accessToken !== '') {
  1135 + if (Object.keys(entityData.credential).length) {
  1136 + let credentialsType: DeviceCredentialsType;
  1137 + let credentialsId: string = null;
  1138 + let credentialsValue: string = null;
  1139 + if (isDefinedAndNotNull(entityData.credential.mqtt)) {
  1140 + credentialsType = DeviceCredentialsType.MQTT_BASIC;
  1141 + credentialsValue = JSON.stringify(entityData.credential.mqtt);
  1142 + } else if (isDefinedAndNotNull(entityData.credential.lwm2m)) {
  1143 + credentialsType = DeviceCredentialsType.LWM2M_CREDENTIALS;
  1144 + credentialsValue = JSON.stringify(entityData.credential.lwm2m);
  1145 + } else if (isNotEmptyStr(entityData.credential.x509)) {
  1146 + credentialsType = DeviceCredentialsType.X509_CERTIFICATE;
  1147 + credentialsValue = entityData.credential.x509;
  1148 + } else {
  1149 + credentialsType = DeviceCredentialsType.ACCESS_TOKEN;
  1150 + credentialsId = entityData.credential.accessToken;
  1151 + }
1117 1152 observable = this.deviceService.getDeviceCredentials(entityId.id, false, config).pipe(
1118 1153 mergeMap((credentials) => {
1119   - credentials.credentialsId = entityData.accessToken;
1120   - credentials.credentialsType = DeviceCredentialsType.ACCESS_TOKEN;
1121   - credentials.credentialsValue = null;
  1154 + credentials.credentialsId = credentialsId;
  1155 + credentials.credentialsType = credentialsType;
  1156 + credentials.credentialsValue = credentialsValue;
1122 1157 return this.deviceService.saveDeviceCredentials(credentials, config).pipe(
1123 1158 map(() => 'ok'),
1124   - catchError(err => of('error'))
  1159 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1125 1160 );
1126 1161 })
1127 1162 );
... ... @@ -1131,7 +1166,7 @@ export class EntityService {
1131 1166 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SHARED_SCOPE,
1132 1167 entityData.attributes.shared, config).pipe(
1133 1168 map(() => 'ok'),
1134   - catchError(err => of('error'))
  1169 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1135 1170 );
1136 1171 observables.push(observable);
1137 1172 }
... ... @@ -1139,23 +1174,23 @@ export class EntityService {
1139 1174 observable = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE,
1140 1175 entityData.attributes.server, config).pipe(
1141 1176 map(() => 'ok'),
1142   - catchError(err => of('error'))
  1177 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1143 1178 );
1144 1179 observables.push(observable);
1145 1180 }
1146 1181 if (entityData.timeseries && entityData.timeseries.length) {
1147 1182 observable = this.attributeService.saveEntityTimeseries(entityId, 'time', entityData.timeseries, config).pipe(
1148 1183 map(() => 'ok'),
1149   - catchError(err => of('error'))
  1184 + catchError(err => of(`Line: ${entityData.lineNumber}; Error: ${err.error.message}`))
1150 1185 );
1151 1186 observables.push(observable);
1152 1187 }
1153 1188 if (observables.length) {
1154 1189 return forkJoin(observables).pipe(
1155 1190 map((response) => {
1156   - const hasError = response.filter((status) => status === 'error').length > 0;
1157   - if (hasError) {
1158   - throw Error();
  1191 + const hasError = response.filter((status) => status !== 'ok');
  1192 + if (hasError.length > 0) {
  1193 + throw Error(hasError.join('\n'));
1159 1194 } else {
1160 1195 return response;
1161 1196 }
... ...
... ... @@ -94,7 +94,7 @@
94 94 <mat-step [stepControl]="columnTypesFormGroup">
95 95 <form [formGroup]="columnTypesFormGroup">
96 96 <ng-template matStepLabel>{{ 'import.stepper-text.column-type' | translate }}</ng-template>
97   - <tb-table-columns-assignment formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment>
  97 + <tb-table-columns-assignment #columnsAssignmentComponent formControlName="columnsParam" [entityType]="entityType"></tb-table-columns-assignment>
98 98 </form>
99 99 <div fxLayout="row wrap" fxLayoutAlign="space-between center">
100 100 <button mat-button
... ... @@ -125,9 +125,20 @@
125 125 <p class="mat-body-1" *ngIf="this.statistical?.update && this.statistical?.update.entity">
126 126 {{ translate.instant('import.message.update-entities', {count: this.statistical.update.entity}) }}
127 127 </p>
128   - <p class="mat-body-1" *ngIf="this.statistical?.error && this.statistical?.error.entity">
  128 + <p class="mat-body-1" style="margin-bottom: 0.8em" *ngIf="this.statistical?.error && this.statistical?.error.entity">
129 129 {{ translate.instant('import.message.error-entities', {count: this.statistical.error.entity}) }}
130 130 </p>
  131 + <mat-expansion-panel class="advanced-logs" [expanded]="false"
  132 + *ngIf="this.statistical?.error && this.statistical?.error.entity"
  133 + (opened)="initEditor()">
  134 + <mat-expansion-panel-header [collapsedHeight]="'38px'" [expandedHeight]="'38px'">
  135 + <mat-panel-title>
  136 + <div class="tb-small" translate>import.details</div>
  137 + </mat-panel-title>
  138 + </mat-expansion-panel-header>
  139 + <mat-divider></mat-divider>
  140 + <div #failureDetailsEditor class="tb-failure-details"></div>
  141 + </mat-expansion-panel>
131 142 </div>
132 143 <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
133 144 <button mat-raised-button
... ...
... ... @@ -26,5 +26,30 @@
26 26 .tb-import-progress{
27 27 margin: 7px 0;
28 28 }
  29 +
  30 + .tb-failure-details {
  31 + width: 100%;
  32 + min-width: 300px;
  33 + height: 100%;
  34 + min-height: 50px;
  35 + margin-top: 8px;
  36 + }
  37 +
  38 + .mat-expansion-panel {
  39 + box-shadow: none;
  40 + &.advanced-logs {
  41 + border: 1px groove rgba(0, 0, 0, .25);
  42 + padding: 0;
  43 + margin-bottom: 1.6em;
  44 +
  45 + .mat-expansion-panel-header {
  46 + padding: 0 8px;
  47 + }
  48 +
  49 + .mat-expansion-panel-body {
  50 + padding: 0;
  51 + }
  52 + }
  53 + }
29 54 }
30 55 }
... ...
... ... @@ -14,7 +14,7 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Component, Inject, OnInit, ViewChild } from '@angular/core';
  17 +import { AfterViewInit, Component, ElementRef, Inject, Renderer2, ViewChild } from '@angular/core';
18 18 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
19 19 import { Store } from '@ngrx/store';
20 20 import { AppState } from '@core/core.state';
... ... @@ -34,6 +34,12 @@ import {
34 34 } from '@home/components/import-export/import-export.models';
35 35 import { EdgeImportEntityData, ImportEntitiesResultInfo, ImportEntityData } from '@app/shared/models/entity.models';
36 36 import { ImportExportService } from '@home/components/import-export/import-export.service';
  37 +import { TableColumnsAssignmentComponent } from '@home/components/import-export/table-columns-assignment.component';
  38 +import { getDeviceCredentialMQTTDefault } from '@shared/models/device.models';
  39 +import { isDefinedAndNotNull } from '@core/utils';
  40 +import { getLwm2mSecurityConfigModelsDefault } from '@shared/models/lwm2m-security-config.models';
  41 +import { Ace } from 'ace-builds';
  42 +import { getAce } from '@shared/models/ace/ace.models';
37 43
38 44 export interface ImportDialogCsvData {
39 45 entityType: EntityType;
... ... @@ -48,15 +54,21 @@ export interface ImportDialogCsvData {
48 54 styleUrls: ['./import-dialog-csv.component.scss']
49 55 })
50 56 export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvComponent, boolean>
51   - implements OnInit {
  57 + implements AfterViewInit {
52 58
53 59 @ViewChild('importStepper', {static: true}) importStepper: MatVerticalStepper;
54 60
  61 + @ViewChild('columnsAssignmentComponent', {static: true})
  62 + columnsAssignmentComponent: TableColumnsAssignmentComponent;
  63 +
  64 + @ViewChild('failureDetailsEditor')
  65 + failureDetailsEditorElmRef: ElementRef;
  66 +
55 67 entityType: EntityType;
56 68 importTitle: string;
57 69 importFileLabel: string;
58 70
59   - delimiters: {key: string, value: string}[] = [{
  71 + delimiters: { key: string, value: string }[] = [{
60 72 key: ',',
61 73 value: ','
62 74 }, {
... ... @@ -80,6 +92,8 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
80 92 progressCreate = 0;
81 93 statistical: ImportEntitiesResultInfo;
82 94
  95 + private allowAssignColumn: ImportEntityColumnType[];
  96 + private initEditorComponent = false;
83 97 private parseData: CsvToJsonResult;
84 98
85 99 constructor(protected store: Store<AppState>,
... ... @@ -88,7 +102,8 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
88 102 public dialogRef: MatDialogRef<ImportDialogCsvComponent, boolean>,
89 103 public translate: TranslateService,
90 104 private importExport: ImportExportService,
91   - private fb: FormBuilder) {
  105 + private fb: FormBuilder,
  106 + private renderer: Renderer2) {
92 107 super(store, router, dialogRef);
93 108 this.entityType = data.entityType;
94 109 this.importTitle = data.importTitle;
... ... @@ -109,7 +124,12 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
109 124 });
110 125 }
111 126
112   - ngOnInit(): void {
  127 + ngAfterViewInit() {
  128 + let columns = this.columnsAssignmentComponent.columnTypes;
  129 + if (this.entityType === EntityType.DEVICE) {
  130 + columns = columns.concat(this.columnsAssignmentComponent.columnDeviceCredentials);
  131 + }
  132 + this.allowAssignColumn = columns.map(column => column.value);
113 133 }
114 134
115 135 cancel(): void {
... ... @@ -157,8 +177,10 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
157 177 return convertCSVToJson(importData, config,
158 178 (messageId, params) => {
159 179 this.store.dispatch(new ActionNotificationShow(
160   - {message: this.translate.instant(messageId, params),
161   - type: 'error'}));
  180 + {
  181 + message: this.translate.instant(messageId, params),
  182 + type: 'error'
  183 + }));
162 184 }
163 185 );
164 186 }
... ... @@ -168,9 +190,14 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
168 190 const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value;
169 191 for (let i = 0; i < this.parseData.headers.length; i++) {
170 192 let columnParam: CsvColumnParam;
171   - if (isHeader && this.parseData.headers[i].search(/^(name|type|label)$/im) === 0) {
  193 + let findEntityColumnType: ImportEntityColumnType;
  194 + if (isHeader) {
  195 + const headerColumnName = this.parseData.headers[i].toUpperCase();
  196 + findEntityColumnType = this.allowAssignColumn.find(column => column === headerColumnName);
  197 + }
  198 + if (isHeader && findEntityColumnType) {
172 199 columnParam = {
173   - type: ImportEntityColumnType[this.parseData.headers[i].toLowerCase()],
  200 + type: findEntityColumnType,
174 201 key: this.parseData.headers[i].toLowerCase(),
175 202 sampleData: this.parseData.rows[0][i]
176 203 };
... ... @@ -189,13 +216,19 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
189 216
190 217 private addEntities() {
191 218 const importData = this.parseData;
  219 + const isHeader: boolean = this.importParametersFormGroup.get('isHeader').value;
192 220 const parameterColumns: CsvColumnParam[] = this.columnTypesFormGroup.get('columnsParam').value;
193 221 const entitiesData: ImportEntityData[] = [];
194 222 let sentDataLength = 0;
  223 + const startLineNumber = isHeader ? 2 : 1;
195 224 for (let row = 0; row < importData.rows.length; row++) {
196 225 const entityData: ImportEntityData = this.constructDraftImportEntityData();
197 226 const i = row;
  227 + entityData.lineNumber = startLineNumber + i;
198 228 for (let j = 0; j < parameterColumns.length; j++) {
  229 + if (!isDefinedAndNotNull(importData.rows[i][j]) || importData.rows[i][j] === '') {
  230 + continue;
  231 + }
199 232 switch (parameterColumns[j].type) {
200 233 case ImportEntityColumnType.serverAttribute:
201 234 entityData.attributes.server.push({
... ... @@ -215,9 +248,6 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
215 248 value: importData.rows[i][j]
216 249 });
217 250 break;
218   - case ImportEntityColumnType.accessToken:
219   - entityData.accessToken = importData.rows[i][j];
220   - break;
221 251 case ImportEntityColumnType.name:
222 252 entityData.name = importData.rows[i][j];
223 253 break;
... ... @@ -233,6 +263,96 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
233 263 case ImportEntityColumnType.description:
234 264 entityData.description = importData.rows[i][j];
235 265 break;
  266 + case ImportEntityColumnType.accessToken:
  267 + entityData.credential.accessToken = importData.rows[i][j];
  268 + break;
  269 + case ImportEntityColumnType.x509:
  270 + entityData.credential.x509 = importData.rows[i][j];
  271 + break;
  272 + case ImportEntityColumnType.mqttClientId:
  273 + if (!entityData.credential.mqtt) {
  274 + entityData.credential.mqtt = getDeviceCredentialMQTTDefault();
  275 + }
  276 + entityData.credential.mqtt.clientId = importData.rows[i][j];
  277 + break;
  278 + case ImportEntityColumnType.mqttUserName:
  279 + if (!entityData.credential.mqtt) {
  280 + entityData.credential.mqtt = getDeviceCredentialMQTTDefault();
  281 + }
  282 + entityData.credential.mqtt.userName = importData.rows[i][j];
  283 + break;
  284 + case ImportEntityColumnType.mqttPassword:
  285 + if (!entityData.credential.mqtt) {
  286 + entityData.credential.mqtt = getDeviceCredentialMQTTDefault();
  287 + }
  288 + entityData.credential.mqtt.password = importData.rows[i][j];
  289 + break;
  290 + case ImportEntityColumnType.lwm2mClientEndpoint:
  291 + if (!entityData.credential.lwm2m) {
  292 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  293 + }
  294 + entityData.credential.lwm2m.client.endpoint = importData.rows[i][j];
  295 + break;
  296 + case ImportEntityColumnType.lwm2mClientSecurityConfigMode:
  297 + if (!entityData.credential.lwm2m) {
  298 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  299 + }
  300 + entityData.credential.lwm2m.client.securityConfigClientMode = importData.rows[i][j];
  301 + break;
  302 + case ImportEntityColumnType.lwm2mClientIdentity:
  303 + if (!entityData.credential.lwm2m) {
  304 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  305 + }
  306 + entityData.credential.lwm2m.client.identity = importData.rows[i][j];
  307 + break;
  308 + case ImportEntityColumnType.lwm2mClientKey:
  309 + if (!entityData.credential.lwm2m) {
  310 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  311 + }
  312 + entityData.credential.lwm2m.client.key = importData.rows[i][j];
  313 + break;
  314 + case ImportEntityColumnType.lwm2mClientCert:
  315 + if (!entityData.credential.lwm2m) {
  316 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  317 + }
  318 + entityData.credential.lwm2m.client.cert = importData.rows[i][j];
  319 + break;
  320 + case ImportEntityColumnType.lwm2mBootstrapServerSecurityMode:
  321 + if (!entityData.credential.lwm2m) {
  322 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  323 + }
  324 + entityData.credential.lwm2m.bootstrap.bootstrapServer.securityMode = importData.rows[i][j];
  325 + break;
  326 + case ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId:
  327 + if (!entityData.credential.lwm2m) {
  328 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  329 + }
  330 + entityData.credential.lwm2m.bootstrap.bootstrapServer.clientPublicKeyOrId = importData.rows[i][j];
  331 + break;
  332 + case ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey:
  333 + if (!entityData.credential.lwm2m) {
  334 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  335 + }
  336 + entityData.credential.lwm2m.bootstrap.bootstrapServer.clientSecretKey = importData.rows[i][j];
  337 + break;
  338 + case ImportEntityColumnType.lwm2mServerSecurityMode:
  339 + if (!entityData.credential.lwm2m) {
  340 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  341 + }
  342 + entityData.credential.lwm2m.bootstrap.lwm2mServer.securityMode = importData.rows[i][j];
  343 + break;
  344 + case ImportEntityColumnType.lwm2mServerClientPublicKeyOrId:
  345 + if (!entityData.credential.lwm2m) {
  346 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  347 + }
  348 + entityData.credential.lwm2m.bootstrap.lwm2mServer.clientPublicKeyOrId = importData.rows[i][j];
  349 + break;
  350 + case ImportEntityColumnType.lwm2mServerClientSecretKey:
  351 + if (!entityData.credential.lwm2m) {
  352 + entityData.credential.lwm2m = getLwm2mSecurityConfigModelsDefault();
  353 + }
  354 + entityData.credential.lwm2m.bootstrap.lwm2mServer.clientSecretKey = importData.rows[i][j];
  355 + break;
236 356 case ImportEntityColumnType.edgeLicenseKey:
237 357 (entityData as EdgeImportEntityData).edgeLicenseKey = importData.rows[i][j];
238 358 break;
... ... @@ -268,16 +388,17 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
268 388
269 389 private constructDraftImportEntityData(): ImportEntityData {
270 390 const entityData: ImportEntityData = {
  391 + lineNumber: 1,
271 392 name: '',
272 393 type: '',
273 394 description: '',
274 395 gateway: null,
275 396 label: '',
276   - accessToken: '',
277 397 attributes: {
278 398 server: [],
279 399 shared: []
280 400 },
  401 + credential: {},
281 402 timeseries: []
282 403 };
283 404 if (this.entityType === EntityType.EDGE) {
... ... @@ -292,5 +413,49 @@ export class ImportDialogCsvComponent extends DialogComponent<ImportDialogCsvCom
292 413 }
293 414 }
294 415
  416 + initEditor() {
  417 + if (!this.initEditorComponent) {
  418 + this.createEditor(this.failureDetailsEditorElmRef, this.statistical.error.errors);
  419 + }
  420 + }
  421 +
  422 + private createEditor(editorElementRef: ElementRef, content: string): void {
  423 + const editorElement = editorElementRef.nativeElement;
  424 + let editorOptions: Partial<Ace.EditorOptions> = {
  425 + mode: 'ace/mode/java',
  426 + theme: 'ace/theme/github',
  427 + showGutter: false,
  428 + showPrintMargin: false,
  429 + readOnly: true
  430 + };
  431 +
  432 + const advancedOptions = {
  433 + enableSnippets: false,
  434 + enableBasicAutocompletion: false,
  435 + enableLiveAutocompletion: false
  436 + };
  437 +
  438 + editorOptions = {...editorOptions, ...advancedOptions};
  439 + getAce().subscribe(
  440 + (ace) => {
  441 + const editor = ace.edit(editorElement, editorOptions);
  442 + editor.session.setUseWrapMode(false);
  443 + editor.setValue(content, -1);
  444 + this.updateEditorSize(editorElement, content, editor);
  445 + }
  446 + );
  447 + }
  448 +
  449 + private updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) {
  450 + let newHeight = 200;
  451 + if (content && content.length > 0) {
  452 + const lines = content.split('\n');
  453 + newHeight = 16 * lines.length + 24;
  454 + }
  455 + const minHeight = Math.min(200, newHeight);
  456 + this.renderer.setStyle(editorElement, 'minHeight', minHeight.toString() + 'px');
  457 + this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px');
  458 + editor.resize();
  459 + }
295 460
296 461 }
... ...
... ... @@ -14,7 +14,7 @@
14 14 /// limitations under the License.
15 15 ///
16 16
17   -import { Widget, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models';
  17 +import { Widget, WidgetTypeDetails } from '@app/shared/models/widget.models';
18 18 import { DashboardLayoutId } from '@shared/models/dashboard.models';
19 19 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
20 20
... ... @@ -46,8 +46,22 @@ export enum ImportEntityColumnType {
46 46 sharedAttribute = 'SHARED_ATTRIBUTE',
47 47 serverAttribute = 'SERVER_ATTRIBUTE',
48 48 timeseries = 'TIMESERIES',
49   - entityField = 'ENTITY_FIELD',
50 49 accessToken = 'ACCESS_TOKEN',
  50 + x509 = 'X509',
  51 + mqttClientId = 'MQTT_CLIENT_ID',
  52 + mqttUserName = 'MQTT_USER_NAME',
  53 + mqttPassword = 'MQTT_PASSWORD',
  54 + lwm2mClientEndpoint = 'LWM2M_CLIENT_ENDPOINT',
  55 + lwm2mClientSecurityConfigMode = 'LWM2M_CLIENT_SECURITY_CONFIG_MODE',
  56 + lwm2mClientIdentity = 'LWM2M_CLIENT_IDENTITY',
  57 + lwm2mClientKey = 'LWM2M_CLIENT_KEY',
  58 + lwm2mClientCert = 'LWM2M_CLIENT_CERT',
  59 + lwm2mBootstrapServerSecurityMode = 'LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE',
  60 + lwm2mBootstrapServerClientPublicKeyOrId = 'LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID',
  61 + lwm2mBootstrapServerClientSecretKey = 'LWM2M_BOOTSTRAP_SERVER_SECRET_KEY',
  62 + lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE',
  63 + lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID',
  64 + lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY',
51 65 isGateway = 'IS_GATEWAY',
52 66 description = 'DESCRIPTION',
53 67 edgeLicenseKey = 'EDGE_LICENSE_KEY',
... ... @@ -68,8 +82,22 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
68 82 [ImportEntityColumnType.sharedAttribute, 'import.column-type.shared-attribute'],
69 83 [ImportEntityColumnType.serverAttribute, 'import.column-type.server-attribute'],
70 84 [ImportEntityColumnType.timeseries, 'import.column-type.timeseries'],
71   - [ImportEntityColumnType.entityField, 'import.column-type.entity-field'],
72 85 [ImportEntityColumnType.accessToken, 'import.column-type.access-token'],
  86 + [ImportEntityColumnType.x509, 'import.column-type.x509'],
  87 + [ImportEntityColumnType.mqttClientId, 'import.column-type.mqtt.client-id'],
  88 + [ImportEntityColumnType.mqttUserName, 'import.column-type.mqtt.user-name'],
  89 + [ImportEntityColumnType.mqttPassword, 'import.column-type.mqtt.password'],
  90 + [ImportEntityColumnType.lwm2mClientEndpoint, 'import.column-type.lwm2m.client-endpoint'],
  91 + [ImportEntityColumnType.lwm2mClientSecurityConfigMode, 'import.column-type.lwm2m.security-config-mode'],
  92 + [ImportEntityColumnType.lwm2mClientIdentity, 'import.column-type.lwm2m.client-identity'],
  93 + [ImportEntityColumnType.lwm2mClientKey, 'import.column-type.lwm2m.client-key'],
  94 + [ImportEntityColumnType.lwm2mClientCert, 'import.column-type.lwm2m.client-cert'],
  95 + [ImportEntityColumnType.lwm2mBootstrapServerSecurityMode, 'import.column-type.lwm2m.bootstrap-server-security-mode'],
  96 + [ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId, 'import.column-type.lwm2m.bootstrap-server-public-key-id'],
  97 + [ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey, 'import.column-type.lwm2m.bootstrap-server-secret-key'],
  98 + [ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'],
  99 + [ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'],
  100 + [ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'],
73 101 [ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
74 102 [ImportEntityColumnType.description, 'import.column-type.description'],
75 103 [ImportEntityColumnType.edgeLicenseKey, 'import.column-type.edge-license-key'],
... ...
... ... @@ -21,7 +21,7 @@ import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22 22 import { ActionNotificationShow } from '@core/notification/notification.actions';
23 23 import { Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models';
24   -import { deepClone, isDefined, isObject, isUndefined } from '@core/utils';
  24 +import { deepClone, isDefined, isObject, isString, isUndefined } from '@core/utils';
25 25 import { WINDOW } from '@core/services/window.service';
26 26 import { DOCUMENT } from '@angular/common';
27 27 import {
... ... @@ -563,6 +563,8 @@ export class ImportExportService {
563 563 if (isObject(obj2[key])) {
564 564 obj1[key] = obj1[key] || {};
565 565 obj1[key] = {...obj1[key], ...this.sumObject(obj1[key], obj2[key])};
  566 + } else if (isString(obj2[key])) {
  567 + obj1[key] = (obj1[key] || '') + `${obj2[key]}\n`;
566 568 } else {
567 569 obj1[key] = (obj1[key] || 0) + obj2[key];
568 570 }
... ...
... ... @@ -31,10 +31,15 @@
31 31 <ng-container matColumnDef="type">
32 32 <mat-header-cell *matHeaderCellDef style="flex: 0 0 40%" class="mat-column-type"> {{ 'import.column-type.column-type' | translate }} </mat-header-cell>
33 33 <mat-cell *matCellDef="let column">
34   - <mat-select matInput [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
  34 + <mat-select [(ngModel)]="column.type" (ngModelChange)="columnsUpdated()">
35 35 <mat-option *ngFor="let type of columnTypes" [value]="type.value" [disabled]="type.disabled">
36 36 {{ columnTypesTranslations.get(type.value) | translate }}
37 37 </mat-option>
  38 + <mat-optgroup label="{{ 'import.credentials' | translate }}" *ngIf="entityType === entityTypeDevice">
  39 + <mat-option *ngFor="let credential of columnDeviceCredentials" [value]="credential.value" [disabled]="credential.disabled">
  40 + {{ columnTypesTranslations.get(credential.value) | translate }}
  41 + </mat-option>
  42 + </mat-optgroup>
38 43 </mat-select>
39 44 </mat-cell>
40 45 </ng-container>
... ...
... ... @@ -57,8 +57,12 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
57 57
58 58 columnTypes: AssignmentColumnType[] = [];
59 59
  60 + columnDeviceCredentials: AssignmentColumnType[] = [];
  61 +
60 62 columnTypesTranslations = importEntityColumnTypeTranslations;
61 63
  64 + readonly entityTypeDevice = EntityType.DEVICE;
  65 +
62 66 private columns: CsvColumnParam[];
63 67
64 68 private valid = true;
... ... @@ -83,9 +87,26 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
83 87 { value: ImportEntityColumnType.sharedAttribute },
84 88 { value: ImportEntityColumnType.serverAttribute },
85 89 { value: ImportEntityColumnType.timeseries },
86   - { value: ImportEntityColumnType.accessToken },
87 90 { value: ImportEntityColumnType.isGateway }
88 91 );
  92 + this.columnDeviceCredentials.push(
  93 + { value: ImportEntityColumnType.accessToken },
  94 + { value: ImportEntityColumnType.x509 },
  95 + { value: ImportEntityColumnType.mqttClientId },
  96 + { value: ImportEntityColumnType.mqttUserName },
  97 + { value: ImportEntityColumnType.mqttPassword },
  98 + { value: ImportEntityColumnType.lwm2mClientEndpoint },
  99 + { value: ImportEntityColumnType.lwm2mClientSecurityConfigMode },
  100 + { value: ImportEntityColumnType.lwm2mClientIdentity },
  101 + { value: ImportEntityColumnType.lwm2mClientKey },
  102 + { value: ImportEntityColumnType.lwm2mClientCert },
  103 + { value: ImportEntityColumnType.lwm2mBootstrapServerSecurityMode },
  104 + { value: ImportEntityColumnType.lwm2mBootstrapServerClientPublicKeyOrId },
  105 + { value: ImportEntityColumnType.lwm2mBootstrapServerClientSecretKey },
  106 + { value: ImportEntityColumnType.lwm2mServerSecurityMode },
  107 + { value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId },
  108 + { value: ImportEntityColumnType.lwm2mServerClientSecretKey },
  109 + );
89 110 break;
90 111 case EntityType.ASSET:
91 112 this.columnTypes.push(
... ... @@ -123,8 +144,6 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
123 144 const isSelectName = this.columns.findIndex((column) => column.type === ImportEntityColumnType.name) > -1;
124 145 const isSelectType = this.columns.findIndex((column) => column.type === ImportEntityColumnType.type) > -1;
125 146 const isSelectLabel = this.columns.findIndex((column) => column.type === ImportEntityColumnType.label) > -1;
126   - const isSelectCredentials = this.columns.findIndex((column) => column.type === ImportEntityColumnType.accessToken) > -1;
127   - const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1;
128 147 const isSelectDescription = this.columns.findIndex((column) => column.type === ImportEntityColumnType.description) > -1;
129 148 const isSelectEdgeLicenseKey = this.columns.findIndex((column) => column.type === ImportEntityColumnType.edgeLicenseKey) > -1;
130 149 const isSelectCloudEndpoint = this.columns.findIndex((column) => column.type === ImportEntityColumnType.cloudEndpoint) > -1;
... ... @@ -139,14 +158,19 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
139 158 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.label).disabled = isSelectLabel;
140 159 this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.description).disabled = isSelectDescription;
141 160
142   - const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway);
143   - if (isGatewayColumnType) {
144   - isGatewayColumnType.disabled = isSelectGateway;
145   - }
146   - const accessTokenColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.accessToken);
147   - if (accessTokenColumnType) {
148   - accessTokenColumnType.disabled = isSelectCredentials;
  161 + if (this.entityType === EntityType.DEVICE) {
  162 + const isSelectGateway = this.columns.findIndex((column) => column.type === ImportEntityColumnType.isGateway) > -1;
  163 +
  164 + const isGatewayColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.isGateway);
  165 + if (isGatewayColumnType) {
  166 + isGatewayColumnType.disabled = isSelectGateway;
  167 + }
  168 +
  169 + this.columnDeviceCredentials.forEach((columnCredential) => {
  170 + columnCredential.disabled = this.columns.findIndex(column => column.type === columnCredential.value) > -1;
  171 + });
149 172 }
  173 +
150 174 const edgeLicenseKeyColumnType = this.columnTypes.find((columnType) => columnType.value === ImportEntityColumnType.edgeLicenseKey);
151 175 if (edgeLicenseKeyColumnType) {
152 176 edgeLicenseKeyColumnType.disabled = isSelectEdgeLicenseKey;
... ...
... ... @@ -743,6 +743,14 @@ export interface DeviceCredentialMQTTBasic {
743 743 password: string;
744 744 }
745 745
  746 +export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic {
  747 + return {
  748 + clientId: '',
  749 + userName: '',
  750 + password: ''
  751 + };
  752 +}
  753 +
746 754 export interface DeviceSearchQuery extends EntitySearchQuery {
747 755 deviceTypes: Array<string>;
748 756 }
... ...
... ... @@ -17,6 +17,8 @@
17 17 import { EntityType } from '@shared/models/entity-type.models';
18 18 import { AttributeData } from './telemetry/telemetry.models';
19 19 import { EntityId } from '@shared/models/id/entity-id';
  20 +import { DeviceCredentialMQTTBasic } from '@shared/models/device.models';
  21 +import { Lwm2mSecurityConfigModels } from '@shared/models/lwm2m-security-config.models';
20 22
21 23 export interface EntityInfo {
22 24 name?: string;
... ... @@ -32,12 +34,18 @@ export interface EntityInfoData {
32 34 }
33 35
34 36 export interface ImportEntityData {
  37 + lineNumber: number;
35 38 name: string;
36 39 type: string;
37 40 label: string;
38 41 gateway: boolean;
39 42 description: string;
40   - accessToken: string;
  43 + credential: {
  44 + accessToken?: string;
  45 + x509?: string;
  46 + mqtt?: DeviceCredentialMQTTBasic;
  47 + lwm2m?: Lwm2mSecurityConfigModels;
  48 + };
41 49 attributes: {
42 50 server: AttributeData[],
43 51 shared: AttributeData[]
... ... @@ -61,6 +69,7 @@ export interface ImportEntitiesResultInfo {
61 69 };
62 70 error?: {
63 71 entity: number;
  72 + errors?: string;
64 73 };
65 74 }
66 75
... ...
... ... @@ -60,6 +60,20 @@ export interface Lwm2mSecurityConfigModels {
60 60 bootstrap: BootstrapSecurityConfig;
61 61 }
62 62
  63 +
  64 +export function getLwm2mSecurityConfigModelsDefault(): Lwm2mSecurityConfigModels {
  65 + return {
  66 + client: {
  67 + securityConfigClientMode: Lwm2mSecurityType.NO_SEC,
  68 + endpoint: ''
  69 + },
  70 + bootstrap: {
  71 + bootstrapServer: getDefaultServerSecurityConfig(),
  72 + lwm2mServer: getDefaultServerSecurityConfig()
  73 + }
  74 + };
  75 +}
  76 +
63 77 export function getDefaultClientSecurityConfig(securityConfigMode: Lwm2mSecurityType, endPoint = ''): ClientSecurityConfig {
64 78 let security = {
65 79 securityConfigClientMode: securityConfigMode,
... ...
... ... @@ -2184,9 +2184,11 @@
2184 2184 "column-title": "Title",
2185 2185 "column-example": "Example value data",
2186 2186 "column-key": "Attribute/telemetry key",
  2187 + "credentials": "Credentials",
2187 2188 "csv-delimiter": "CSV delimiter",
2188 2189 "csv-first-line-header": "First line contains column names",
2189 2190 "csv-update-data": "Update attributes/telemetry",
  2191 + "details": "Details",
2190 2192 "import-csv-number-columns-error": "A file should contain at least two columns",
2191 2193 "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'",
2192 2194 "column-type": {
... ... @@ -2200,6 +2202,25 @@
2200 2202 "timeseries": "Timeseries",
2201 2203 "entity-field": "Entity field",
2202 2204 "access-token": "Access token",
  2205 + "x509": "X.509",
  2206 + "mqtt": {
  2207 + "client-id": "MQTT client ID",
  2208 + "user-name": "MQTT user name",
  2209 + "password": "MQTT password"
  2210 + },
  2211 + "lwm2m": {
  2212 + "client-endpoint": "LwM2M endpoint client name",
  2213 + "security-config-mode": "LwM2M security config mode",
  2214 + "client-identity": "LwM2M client identity",
  2215 + "client-key": "LwM2M client key",
  2216 + "client-cert": "LwM2M client public key",
  2217 + "bootstrap-server-security-mode": "LwM2M bootstrap server security mode",
  2218 + "bootstrap-server-secret-key": "LwM2M bootstrap server secret key",
  2219 + "bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id",
  2220 + "lwm2m-server-security-mode": "LwM2M server security mode",
  2221 + "lwm2m-server-secret-key": "LwM2M server secret key",
  2222 + "lwm2m-server-public-key-id": "LwM2M server public key or id"
  2223 + },
2203 2224 "isgateway": "Is Gateway",
2204 2225 "activity-time-from-gateway-device": "Activity time from gateway device",
2205 2226 "description": "Description",
... ...