Skip to content

Commit ced9d30

Browse files
authored
Merge pull request #74 from OS2iot/feature/759-iot-device-metadata
Add metadata to IoT devices
2 parents 8a88110 + 0830742 commit ced9d30

18 files changed

+304
-18115
lines changed

package-lock.json

Lines changed: 23 additions & 18078 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/app.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function tokenGetter() {
3636
@NgModule({
3737
declarations: [
3838
AppComponent,
39-
ErrorPageComponent
39+
ErrorPageComponent,
4040
],
4141
imports: [
4242
SharedVariableModule.forRoot(),

src/app/applications/iot-devices/iot-device-detail/iot-device-detail-generic/iot-device-detail-generic.component.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ <h3>{{ 'IOTDEVICE.DETAIL' | translate }}</h3>
4040
{{ 'IOTDEVICE.NOCOMMENT' | translate}}
4141
</ng-template>
4242
</p>
43+
<ng-container *ngIf="metadataTags.length">
44+
<mat-divider></mat-divider>
45+
<p *ngFor="let tag of metadataTags">
46+
<strong>{{ tag.key }}</strong>
47+
<span>{{ tag.value }}</span>
48+
</p>
49+
</ng-container>
4350
</div>
4451
</div>
4552
<div class="col-md-6 d-flex align-items-stretch">
@@ -72,4 +79,4 @@ <h3>{{ 'IOTDEVICE.LOCATION' | translate }}</h3>
7279
</p>
7380
</div>
7481
</div>
75-
</div>
82+
</div>
Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,29 @@
1-
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core';
1+
import { Location } from '@angular/common';
2+
import {
3+
Component,
4+
EventEmitter,
5+
Input,
6+
OnChanges,
7+
OnDestroy,
8+
OnInit,
9+
SimpleChanges,
10+
} from '@angular/core';
211
import { IotDevice } from '@applications/iot-devices/iot-device.model';
312
import { IoTDeviceService } from '@applications/iot-devices/iot-device.service';
413
import { TranslateService } from '@ngx-translate/core';
5-
import { Location } from '@angular/common';
6-
import { DeviceType } from '@shared/enums/device-type';
7-
import { MatDialog } from '@angular/material/dialog';
8-
import { DeleteDialogComponent } from '@shared/components/delete-dialog/delete-dialog.component';
9-
import { Subscription } from 'rxjs';
10-
import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
14+
import { jsonToList } from '@shared/helpers/json.helper';
15+
import { KeyValue } from '@shared/types/tuple.type';
1116

1217
@Component({
1318
selector: 'app-iot-device-detail-generic',
1419
templateUrl: './iot-device-detail-generic.component.html',
15-
styleUrls: ['./iot-device-detail-generic.component.scss']
20+
styleUrls: ['./iot-device-detail-generic.component.scss'],
1621
})
17-
export class IotDeviceDetailGenericComponent implements OnInit, OnChanges, OnDestroy {
22+
export class IotDeviceDetailGenericComponent
23+
implements OnInit, OnChanges, OnDestroy {
1824
batteryStatusColor = 'green';
1925
batteryStatusPercentage: number;
26+
metadataTags: KeyValue[] = [];
2027
@Input() device: IotDevice;
2128
@Input() latitude = 0;
2229
@Input() longitude = 0;
@@ -27,16 +34,21 @@ export class IotDeviceDetailGenericComponent implements OnInit, OnChanges, OnDes
2734
constructor(
2835
private translate: TranslateService,
2936
public iotDeviceService: IoTDeviceService,
30-
private location: Location,
37+
private location: Location
38+
) {}
3139

32-
) { }
33-
34-
ngOnInit(): void {
35-
}
40+
ngOnInit(): void {}
3641

37-
ngOnChanges(): void {
42+
ngOnChanges(changes: SimpleChanges): void {
3843
this.batteryStatusPercentage = this.getBatteryProcentage();
3944

45+
if (
46+
changes?.device?.previousValue?.metadata !==
47+
changes?.device?.currentValue?.metadata &&
48+
this.device.metadata
49+
) {
50+
this.metadataTags = jsonToList(this.device.metadata);
51+
}
4052
}
4153

4254
routeBack(): void {
@@ -48,19 +60,19 @@ export class IotDeviceDetailGenericComponent implements OnInit, OnChanges, OnDes
4860
latitude: this.latitude,
4961
draggable: false,
5062
editEnabled: false,
51-
useGeolocation: false
63+
useGeolocation: false,
5264
};
5365
}
5466

5567
getBatteryProcentage(): number {
56-
if (this.device?.lorawanSettings?.deviceStatusBattery === this.CHIRPSTACK_BATTERY_NOT_AVAILIBLE) {
68+
if (
69+
this.device?.lorawanSettings?.deviceStatusBattery ===
70+
this.CHIRPSTACK_BATTERY_NOT_AVAILIBLE
71+
) {
5772
return null;
5873
}
5974
return Math.round(this.device?.lorawanSettings?.deviceStatusBattery);
6075
}
6176

62-
ngOnDestroy(): void {
63-
64-
}
65-
77+
ngOnDestroy(): void {}
6678
}

src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@ <h3>{{'IOTDEVICE.LORAWANSETUP' | translate}}</h3>
123123
<label class="form-label" for="serviceProfileID">{{'QUESTION.CHOOSE-SERVICEPROFILE' | translate}}</label>*
124124
<select id="serviceProfileID" class="form-select" name="serviceProfileID"
125125
[(ngModel)]="iotDevice.lorawanSettings.serviceProfileID" required
126-
[value]="iotDevice.lorawanSettings.serviceProfileID"
127-
[disabled]="editmode"
126+
[value]="iotDevice.lorawanSettings.serviceProfileID" [disabled]="editmode"
128127
[ngClass]="{'is-invalid' : formFailedSubmit && errorFields.includes('serviceProfileID'), 'is-valid' : formFailedSubmit && !errorFields.includes('serviceProfileID')}">
129128
<option *ngFor="let serviceProfile of serviceProfiles" [value]="serviceProfile.id"
130129
[selected]="serviceProfile.id === iotDevice.lorawanSettings.serviceProfileID">
@@ -181,8 +180,8 @@ <h3>{{'QUESTION.ABP' | translate}}</h3>
181180
</div>
182181

183182
<div class="form-group mt-5">
184-
<label class="form-label"
185-
for="applicationSessionKey">{{'QUESTION.APPLICATIONSESSIONKEY' | translate}}*</label>
183+
<label class="form-label" for="applicationSessionKey">{{'QUESTION.APPLICATIONSESSIONKEY' |
184+
translate}}*</label>
186185
<input type="text" id="applicationSessionKey" name="applicationSessionKey" maxLength="32"
187186
[placeholder]="'QUESTION.APPLICATIONSESSIONKEY-PLACEHOLDER' | translate" class="form-control"
188187
[(ngModel)]="iotDevice.lorawanSettings.applicationSessionKey"
@@ -222,8 +221,13 @@ <h3>{{'QUESTION.ABP' | translate}}</h3>
222221
</app-sigfox-device-edit>
223222
</ng-container>
224223

224+
<div class="mt-5 row">
225+
<h3>{{ "QUESTION.METADATA" | translate}}</h3>
226+
<app-form-key-value-list [(tags)]="metadataTags" [errorFieldId]="errorMetadataFieldId">
227+
</app-form-key-value-list>
228+
</div>
225229
<div class="form-group mt-5">
226230
<button (click)="routeBack()" class="btn btn-secondary" type="button">{{ 'GEN.CANCEL' | translate}}</button>
227231
<button class="btn btn-primary ml-2" type="submit">{{ 'GEN.SAVE' | translate }}</button>
228232
</div>
229-
</form>
233+
</form>

src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ServiceProfileService } from '@profiles/service-profiles/service-profil
1515
import { ActivationType } from '@shared/enums/activation-type';
1616
import { DeviceType } from '@shared/enums/device-type';
1717
import { ErrorMessageService } from '@shared/error-message.service';
18+
import { jsonToList } from '@shared/helpers/json.helper';
1819
import { ErrorMessage } from '@shared/models/error-message.model';
1920
import { ScrollToTopService } from '@shared/services/scroll-to-top.service';
2021
import { SharedVariableService } from '@shared/shared-variable/shared-variable.service';
@@ -43,6 +44,8 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
4344
iotDevice = new IotDevice();
4445
editmode = false;
4546
public OTAA = true;
47+
metadataTags: {key?: string, value?: string}[] = [];
48+
errorMetadataFieldId: string | undefined;
4649

4750
public deviceSubscription: Subscription;
4851
private applicationsSubscription: Subscription;
@@ -95,7 +98,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
9598
getDeviceModels() {
9699
this.deviceModelService.getMultiple(
97100
1000,
98-
0,
101+
0,
99102
"id",
100103
"ASC",
101104
this.shareVariable.getSelectedOrganisationId()
@@ -132,6 +135,9 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
132135
if (!device.deviceModelId || device.deviceModelId === null) {
133136
this.iotDevice.deviceModelId = 0;
134137
}
138+
if (device.metadata) {
139+
this.metadataTags = jsonToList(device.metadata);
140+
}
135141
});
136142
}
137143

@@ -177,13 +183,40 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
177183

178184
onSubmit(): void {
179185
this.adjustModelBasedOnType();
186+
187+
if (this.isMetadataSet()) {
188+
const invalidKey = this.validateMetadata();
189+
190+
if (!invalidKey) {
191+
this.setMetadata();
192+
} else {
193+
this.handleMetadataError(invalidKey);
194+
return;
195+
}
196+
}
197+
180198
if (this.deviceId !== 0) {
181-
this.updateIoTDevice(this.deviceId);
199+
this.updateIoTDevice(this.deviceId);
182200
} else {
183-
this.postIoTDevice();
201+
this.postIoTDevice();
184202
}
185203
}
186204

205+
private handleMetadataError(invalidKey: string) {
206+
this.handleError({
207+
error: {
208+
message: [
209+
{
210+
field: 'metadata',
211+
message: 'MESSAGE.DUPLICATE-METADATA-KEY',
212+
},
213+
],
214+
},
215+
});
216+
this.errorMetadataFieldId = invalidKey;
217+
this.formFailedSubmit = true;
218+
}
219+
187220
setActivationType() {
188221
if (this.OTAA) {
189222
this.iotDevice.lorawanSettings.activationType = ActivationType.OTAA;
@@ -220,6 +253,36 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
220253
}
221254
}
222255

256+
private isMetadataSet(): boolean {
257+
return this.metadataTags.length && this.metadataTags.some((tag) => tag.key && tag.value);
258+
}
259+
260+
private validateMetadata(): string | undefined {
261+
const seen = new Set();
262+
263+
for (const tag of this.metadataTags) {
264+
if (seen.size === seen.add(tag.key).size) {
265+
return tag.key;
266+
}
267+
}
268+
}
269+
270+
private setMetadata(): void {
271+
if (
272+
this.metadataTags.length &&
273+
this.metadataTags.some((tag) => tag.key && tag.value)
274+
) {
275+
const metadata: Record<string, string> = {};
276+
this.metadataTags.forEach((tag) => {
277+
if (!tag.key) {
278+
return;
279+
}
280+
metadata[tag.key] = tag.value;
281+
});
282+
this.iotDevice.metadata = JSON.stringify(metadata);
283+
}
284+
}
285+
223286
postIoTDevice() {
224287
this.iotDeviceService.createIoTDevice(this.iotDevice).subscribe(
225288
() => {
@@ -252,7 +315,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy {
252315
this.location.back();
253316
}
254317

255-
handleError(error: HttpErrorResponse) {
318+
handleError(error: Pick<HttpErrorResponse, 'error'>) {
256319
if (error?.error?.message == "MESSAGE.OTAA-INFO-MISSING") {
257320
this.errorFields = ["OTAAapplicationKey"];
258321
this.errorMessages = [error?.error?.message];

src/app/applications/iot-devices/iot-device.model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class IotDevice {
1515
comment: string;
1616
type: DeviceType = DeviceType.GENERICHTTP;
1717
receivedMessagesMetadata: ReceivedMessageMetadata[];
18-
metadata?: JSON;
18+
metadata?: string;
1919
apiKey?: string;
2020
id: number;
2121
createdAt: Date;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<button (click)="addNewTag()" class="btn btn-secondary mb-3" type="button">{{ "FORM.ADD-METADATA-ROW" | translate
2+
}}</button>
3+
4+
<table class="table table-striped table-bordered">
5+
<tbody>
6+
<ng-container *ngFor="let pair of tags; let i = index">
7+
<!-- Global table styling breaks if <tr> isn't a direct child of <tbody> -->
8+
<tr app-form-key-value-pair [id]="i" [pair]="pair" [errorFieldId]="errorFieldId" (deletePair)="deleteTag($event)">
9+
</tr>
10+
</ng-container>
11+
</tbody>
12+
</table>

src/app/shared/components/forms/form-key-value/form-key-value-list/form-key-value-list.component.scss

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Component, Input, OnInit } from '@angular/core';
2+
import { KeyValue } from '@shared/types/tuple.type';
3+
4+
@Component({
5+
selector: 'app-form-key-value-list',
6+
templateUrl: './form-key-value-list.component.html',
7+
styleUrls: ['./form-key-value-list.component.scss'],
8+
})
9+
export class FormKeyValueListComponent implements OnInit {
10+
@Input() tags: KeyValue[] = [{}];
11+
@Input() errorFieldId: string | undefined;
12+
13+
constructor() {}
14+
15+
ngOnInit(): void {}
16+
17+
addNewTag(): void {
18+
this.tags.push({});
19+
}
20+
21+
deleteTag(id: number): void {
22+
this.tags = this.tags.filter((_tag, i) => i !== id);
23+
}
24+
}

0 commit comments

Comments
 (0)