diff --git a/src/app/applications/applications-routing.module.ts b/src/app/applications/applications-routing.module.ts index 1e36fb9c..e63d4cff 100644 --- a/src/app/applications/applications-routing.module.ts +++ b/src/app/applications/applications-routing.module.ts @@ -24,6 +24,7 @@ import { IoTDeviceDetailComponent } from "./iot-devices/iot-device-detail/iot-de import { IotDeviceEditComponent } from "./iot-devices/iot-device-edit/iot-device-edit.component"; import { MulticastDetailComponent } from "./multicast/multicast-detail/multicast-detail.component"; import { MulticastEditComponent } from "./multicast/multicast-edit/multicast-edit.component"; +import { IotDeviceCopyComponent } from "./iot-devices/iot-device-copy/iot-device-copy.component"; const applicationRoutes: Routes = [ { @@ -67,6 +68,10 @@ const applicationRoutes: Routes = [ }, ], }, + { + path: "iot-device-copy/:deviceId", + component: IotDeviceCopyComponent, + }, { path: "datatarget-new", component: DatatargetNewComponent }, { path: "datatarget-edit", component: DatatargetEditComponent }, { diff --git a/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.html b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.html new file mode 100644 index 00000000..c3ac604e --- /dev/null +++ b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.html @@ -0,0 +1 @@ + diff --git a/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.scss b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.spec.ts b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.spec.ts new file mode 100644 index 00000000..8fda3882 --- /dev/null +++ b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; + +import { IotDeviceEditComponent } from "../iot-device-edit/iot-device-edit.component"; + +describe("IotDeviceEditComponent", () => { + let component: IotDeviceEditComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [IotDeviceEditComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IotDeviceEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.ts b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.ts new file mode 100644 index 00000000..5ef9b1bc --- /dev/null +++ b/src/app/applications/iot-devices/iot-device-copy/iot-device-copy.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from "@angular/core"; + +@Component({ + selector: "app-iot-device-copy", + templateUrl: "./iot-device-copy.component.html", + styleUrls: ["./iot-device-copy.component.scss"], +}) +export class IotDeviceCopyComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts index 483395ca..b89028b0 100644 --- a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts +++ b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts @@ -16,7 +16,7 @@ import { MeService } from "@shared/services/me.service"; import { OrganizationAccessScope } from "@shared/enums/access-scopes"; import { IotDeviceDetailsService } from "@applications/iot-devices/iot-device-details-service"; import { IoTDeviceChangeApplicationDialogComponent } from "../iot-device-change-application-dialog/iot-device-change-application-dialog.component"; -import { ApplicationDialogModel, IoTDeviceApplicationDialogModel } from "@shared/models/dialog.model"; +import { IoTDeviceApplicationDialogModel } from "@shared/models/dialog.model"; @Component({ selector: "app-iot-device", @@ -56,6 +56,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { private deleteDialogSubscription: Subscription; public dropdownButton: DropdownButton; public canEdit = false; + private copyDeviceButtonId = "COPY-DEVICE"; private resetApiKeyId = "RESET-API-KEY"; private resetApiKeyOption: ExtraDropdownOption; @@ -104,9 +105,10 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { "IOTDEVICE-TABLE-ROW.RESET-API-KEY", "IOTDEVICE.GENERIC_HTTP.RESET-API-KEY", "GEN.CANCEL", + "GEN.BACK" ]) .subscribe(translations => { - this.backButton.label = translations["NAV.APPLICATIONS"]; + this.backButton.label = translations["GEN.BACK"]; this.dropdownButton.label = translations["IOTDEVICE-TABLE-ROW.SHOW-OPTIONS"]; this.titleService.setTitle(translations["TITLE.IOTDEVICE"]); @@ -148,6 +150,14 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { onClick: () => this.onOpenChangeApplicationDialog(), }); }); + + this.translate.get("IOTDEVICE.COPY-SETTINGS-TO-NEW-DEVICE").subscribe(translation => { + this.dropdownButton.extraOptions.push({ + id: this.copyDeviceButtonId, + label: translation, + onClick: () => this.navigateToCopy(), + }); + }); } }); } @@ -191,6 +201,10 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { }); } + navigateToCopy() { + this.router.navigate(["applications", this.application.id, "iot-device-copy", this.deviceId]); + } + ngOnDestroy() { // prevent memory leak by unsubscribing if (this.iotDeviceSubscription) { diff --git a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html index d61f94a6..06b8f8e7 100644 --- a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html +++ b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html @@ -31,7 +31,7 @@ class="form-check-input" required [attr.disabled]="editmode ? '' : null" - (change)="isChecked($event)" + (change)="isDeviceTypeChecked($event)" [checked]="iotDevice.type.toString().includes('LORAWAN')" />
@@ -48,7 +48,7 @@ class="form-check-input" required [attr.disabled]="editmode ? '' : null" - (change)="isChecked($event)" + (change)="isDeviceTypeChecked($event)" [checked]="iotDevice.type.toString().includes('GENERIC_HTTP')" />
@@ -65,7 +65,7 @@ required class="form-check-input" [attr.disabled]="editmode ? '' : null" - (change)="isChecked($event)" + (change)="isDeviceTypeChecked($event)" [checked]="iotDevice.type.toString().includes('MQTT')" />
@@ -82,7 +82,7 @@ class="form-check-input" required [attr.disabled]="editmode ? '' : null" - (change)="isChecked($event)" + (change)="isDeviceTypeChecked($event)" [checked]="iotDevice.type.toString().includes('SIGFOX')" />
@@ -238,7 +238,7 @@

{{ "IOTDEVICE.LORAWANSETUP" | translate }}

[placeholder]="'QUESTION.DEVEUI-PLACEHOLDER' | translate" class="form-control" [(ngModel)]="iotDevice.lorawanSettings.devEUI" - [disabled]="editmode" + [disabled]="editmode && !isDeviceCopy" [ngClass]="{ 'is-invalid': formFailedSubmit && errorFields.includes('devEUI'), 'is-valid': formFailedSubmit && !errorFields.includes('devEUI') @@ -417,6 +417,20 @@

{{ "QUESTION.ABP" | translate }}

{{ "QUESTION.METADATA" | translate }}

+
+ +
+ + {{ "QUESTION.SKIPFCNTCHECK-YES" | translate }} + +
+
diff --git a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts index 1e561111..a14437d9 100644 --- a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts +++ b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts @@ -1,6 +1,6 @@ import { Location } from "@angular/common"; import { HttpErrorResponse } from "@angular/common/http"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, Input, OnDestroy, OnInit } from "@angular/core"; import { Title } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { Application } from "@app/applications/application.model"; @@ -17,11 +17,13 @@ import { jsonToList } from "@shared/helpers/json.helper"; import { ErrorMessage } from "@shared/models/error-message.model"; import { ScrollToTopService } from "@shared/services/scroll-to-top.service"; import { SharedVariableService } from "@shared/shared-variable/shared-variable.service"; -import { Subscription } from "rxjs"; +import { forkJoin, Subscription } from "rxjs"; import { IotDevice } from "../iot-device.model"; import { IoTDeviceService } from "../iot-device.service"; import { MeService } from "@shared/services/me.service"; import { OrganizationAccessScope } from "@shared/enums/access-scopes"; +import { PayloadDeviceDatatargetService } from "@payload-decoder/payload-device-datatarget.service"; +import { PayloadDeviceDatatargetGetManyResponse } from "@payload-decoder/payload-device-data.model"; @Component({ selector: "app-iot-device-edit", @@ -29,6 +31,8 @@ import { OrganizationAccessScope } from "@shared/enums/access-scopes"; styleUrls: ["./iot-device-edit.component.scss"], }) export class IotDeviceEditComponent implements OnInit, OnDestroy { + @Input() isDeviceCopy: boolean = false; + public copyPayloadAndDatatarget: boolean = false; public errorMessages: any; public errorFields: string[]; public formFailedSubmit = false; @@ -57,6 +61,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { private deviceProfileService: DeviceProfileService, private applicationService: ApplicationService, private iotDeviceService: IoTDeviceService, + private datatargetPayloadService: PayloadDeviceDatatargetService, private location: Location, private shareVariable: SharedVariableService, private deviceModelService: DeviceModelService, @@ -113,7 +118,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { }); } - isChecked(event) { + isDeviceTypeChecked(event) { if (event.target.checked) { this.iotDevice.type = event.target.name; } else if (!event.target.checked && this.iotDevice.type.toString().includes(event.target.name)) { @@ -121,6 +126,10 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { } } + isCopyPayloadAndDatatargetChecked(event) { + this.copyPayloadAndDatatarget = event.target.checked; + } + getDevice(id: number): void { this.deviceSubscription = this.iotDeviceService.getIoTDevice(id).subscribe((device: IotDevice) => { this.iotDevice = device; @@ -140,6 +149,44 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { if (device.metadata) { this.metadataTags = jsonToList(device.metadata); } + + //If coming from copy, reset all these properties + if (this.isDeviceCopy) { + this.iotDevice.id = undefined; + this.iotDevice.name = undefined; + this.iotDevice.createdAt = undefined; + this.iotDevice.createdBy = undefined; + this.iotDevice.createdByName = undefined; + this.iotDevice.updatedAt = undefined; + this.iotDevice.updatedBy = undefined; + this.iotDevice.updatedByName = undefined; + this.copyPayloadAndDatatarget = true; + + switch (this.iotDevice.type) { + case DeviceType.GENERIC_HTTP: { + this.iotDevice.apiKey = undefined; + break; + } + case DeviceType.LORAWAN: { + this.iotDevice.lorawanSettings.devEUI = undefined; + this.iotDevice.lorawanSettings.OTAAapplicationKey = undefined; + this.iotDevice.lorawanSettings.applicationSessionKey = undefined; + this.iotDevice.lorawanSettings.networkSessionKey = undefined; + this.iotDevice.lorawanSettings.devAddr = undefined; + this.iotDevice.lorawanSettings.fCntUp = undefined; + this.iotDevice.lorawanSettings.nFCntDown = undefined; + break; + } + case DeviceType.MQTT_INTERNAL_BROKER: { + this.iotDevice.mqttInternalBrokerSettings.caCertificate = undefined; + this.iotDevice.mqttInternalBrokerSettings.deviceCertificate = undefined; + this.iotDevice.mqttInternalBrokerSettings.deviceCertificateKey = undefined; + this.iotDevice.mqttInternalBrokerSettings.mqttpassword = undefined; + this.iotDevice.mqttInternalBrokerSettings.mqtttopicname = undefined; + this.iotDevice.mqttInternalBrokerSettings.mqttusername = undefined; + } + } + } }); } @@ -190,7 +237,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { } } - if (this.deviceId !== 0) { + if (this.deviceId !== 0 && !this.isDeviceCopy) { this.updateIoTDevice(this.deviceId); } else { this.postIoTDevice(); @@ -275,23 +322,49 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { } } + private navigateToDeviceDetails(device: IotDevice) { + this.router.navigate(["applications/" + this.iotDevice.applicationId + "/iot-device/" + device.id + "/details"]); + } + postIoTDevice() { - // Sanitize devEUI for non-hex characters + // Sanitize devEUI if (this.iotDevice.type === DeviceType.LORAWAN && this.iotDevice.lorawanSettings.devEUI) { this.iotDevice.lorawanSettings.devEUI = this.iotDevice.lorawanSettings.devEUI.replace(/[^0-9A-Fa-f]/g, ""); } - this.iotDeviceService.createIoTDevice(this.iotDevice).subscribe( - (response: IotDevice) => { - this.router.navigate([ - "applications/" + this.iotDevice.applicationId + "/iot-device/" + response.id + "/details", - ]); - }, - (error: HttpErrorResponse) => { - this.handleError(error); - this.formFailedSubmit = true; + //First create the device + this.iotDeviceService.createIoTDevice(this.iotDevice).subscribe((createdDevice: IotDevice) => { + if (!this.copyPayloadAndDatatarget) { + this.navigateToDeviceDetails(createdDevice); + return; } - ); + + //If it's the copy device flow, then get all datatargets from the device that we want to copy. + this.datatargetPayloadService + .getByIoTDevice(this.deviceId) + .subscribe((result: PayloadDeviceDatatargetGetManyResponse) => { + //For each of these datatargets, append the copied device to that datatarget. First we make the observables + const appendToDatatargetObservables = result.data.map(element => + this.datatargetPayloadService.appendCopiedIoTDevice(element.id, { deviceId: createdDevice.id }) + ); + + if (appendToDatatargetObservables.length === 0) { + this.navigateToDeviceDetails(createdDevice); + return; + } + + //Forkjoin is running all observables in parallel and when all are done it returns. + forkJoin(appendToDatatargetObservables).subscribe( + () => this.navigateToDeviceDetails(createdDevice), + this.formFailedSubmitHandleError + ); + }, this.formFailedSubmitHandleError); + }, this.formFailedSubmitHandleError); + } + + formFailedSubmitHandleError(error: HttpErrorResponse) { + this.handleError(error); + this.formFailedSubmit = true; } updateIoTDevice(id: number) { @@ -301,8 +374,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { this.routeBack(); }, (error: HttpErrorResponse) => { - this.handleError(error); - this.formFailedSubmit = true; + this.formFailedSubmitHandleError(error); } ); } diff --git a/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.html b/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.html index c334c688..37d54ee8 100644 --- a/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.html +++ b/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.html @@ -16,7 +16,8 @@ matSort matSortActive="name" matSortDirection="asc" - matSortDisableClear> + matSortDisableClear + > {{ "IOTDEVICE-TABLE-ROW.NOT-SUPPORTED-SHORT" | translate }} @@ -30,12 +31,12 @@
{{ "APPLICATION-TABLE.NAME" | translate }}
- + {{ - iotDevice.name - }} + iotDevice.name + }} @@ -45,7 +46,7 @@
{{ "IOT-TABLE.NETWORK-TECHNOLOGY" | translate }}
- + {{ "IOT-DEVICE-TYPES." + iotDevice.type | translate }} @@ -57,7 +58,7 @@ > {{ "GATEWAY.PLACEMENT-LABEL" | translate }}
- + {{ iotDevice.commentOnLocation ? truncateText(iotDevice.commentOnLocation) : "-" }} @@ -69,7 +70,7 @@
{{ "IOTDEVICE.DEVICEMODEL" | translate }}
- + {{ iotDevice.deviceModel?.body?.name ?? "-" }} @@ -81,7 +82,7 @@ > {{ "IOTDEVICE.LORA.DEVICEPROFILE" | translate }}
- + {{ iotDevice.deviceProfileName ?? "-" }} @@ -91,7 +92,7 @@
{{ "IOT-TABLE.DEV-EUI" | translate }}
- + {{ iotDevice.deviceEUI ?? "-" }} @@ -103,7 +104,7 @@ > {{ "IOT-TABLE.APP-KEY" | translate }}
- + {{ iotDevice.OTAAapplicationKey ?? "-" }} @@ -114,7 +115,7 @@
{{ "IOT-TABLE.RSSI" | translate }}
- + @@ -131,7 +132,7 @@
{{ "IOT-TABLE.SNR" | translate }}
- + @@ -147,7 +148,7 @@
{{ "APPLICATION-TABLE.DATA-TARGETS" | translate }}
- + {{ iotDevice.connections?.length ?? 0 }}
@@ -176,7 +177,7 @@
{{ "IOT-TABLE.ACTIVE" | translate }}
- + {{ lastActive(iotDevice) }}
@@ -197,13 +198,18 @@