Commit e3bf26e2a3a6f5c99c0f6523b77cc1b30fe5759e
1 parent
18635bf8
UI: Added support different credentials type to device bulk import
Showing
12 changed files
with
395 additions
and
48 deletions
... | ... | @@ -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; | ... | ... |
... | ... | @@ -709,6 +709,14 @@ export interface DeviceCredentialMQTTBasic { |
709 | 709 | password: string; |
710 | 710 | } |
711 | 711 | |
712 | +export function getDeviceCredentialMQTTDefault(): DeviceCredentialMQTTBasic { | |
713 | + return { | |
714 | + clientId: '', | |
715 | + userName: '', | |
716 | + password: '' | |
717 | + }; | |
718 | +} | |
719 | + | |
712 | 720 | export interface DeviceSearchQuery extends EntitySearchQuery { |
713 | 721 | deviceTypes: Array<string>; |
714 | 722 | } | ... | ... |
... | ... | @@ -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, | ... | ... |
... | ... | @@ -2185,9 +2185,11 @@ |
2185 | 2185 | "column-title": "Title", |
2186 | 2186 | "column-example": "Example value data", |
2187 | 2187 | "column-key": "Attribute/telemetry key", |
2188 | + "credentials": "Credentials", | |
2188 | 2189 | "csv-delimiter": "CSV delimiter", |
2189 | 2190 | "csv-first-line-header": "First line contains column names", |
2190 | 2191 | "csv-update-data": "Update attributes/telemetry", |
2192 | + "details": "Details", | |
2191 | 2193 | "import-csv-number-columns-error": "A file should contain at least two columns", |
2192 | 2194 | "import-csv-invalid-format-error": "Invalid file format. Line: '{{line}}'", |
2193 | 2195 | "column-type": { |
... | ... | @@ -2201,6 +2203,25 @@ |
2201 | 2203 | "timeseries": "Timeseries", |
2202 | 2204 | "entity-field": "Entity field", |
2203 | 2205 | "access-token": "Access token", |
2206 | + "x509": "X.509", | |
2207 | + "mqtt": { | |
2208 | + "client-id": "MQTT client ID", | |
2209 | + "user-name": "MQTT user name", | |
2210 | + "password": "MQTT password" | |
2211 | + }, | |
2212 | + "lwm2m": { | |
2213 | + "client-endpoint": "LwM2M endpoint client name", | |
2214 | + "security-config-mode": "LwM2M security config mode", | |
2215 | + "client-identity": "LwM2M client identity", | |
2216 | + "client-key": "LwM2M client key", | |
2217 | + "client-cert": "LwM2M client public key", | |
2218 | + "bootstrap-server-security-mode": "LwM2M bootstrap server security mode", | |
2219 | + "bootstrap-server-secret-key": "LwM2M bootstrap server secret key", | |
2220 | + "bootstrap-server-public-key-id": "LwM2M bootstrap server public key or id", | |
2221 | + "lwm2m-server-security-mode": "LwM2M server security mode", | |
2222 | + "lwm2m-server-secret-key": "LwM2M server secret key", | |
2223 | + "lwm2m-server-public-key-id": "LwM2M server public key or id" | |
2224 | + }, | |
2204 | 2225 | "isgateway": "Is Gateway", |
2205 | 2226 | "activity-time-from-gateway-device": "Activity time from gateway device", |
2206 | 2227 | "description": "Description", | ... | ... |