diff --git a/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.html b/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.html new file mode 100644 index 00000000..07edced8 --- /dev/null +++ b/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.html @@ -0,0 +1,114 @@ +
+

{{ "IOTDEVICE.CHANGE-APPLICATION.TITLE" | translate }}

+
+ + + + {{ organization.name }} + + + + + + {{ application.name }} + + + + + + {{ "QUESTION.DEVICE-MODEL-SELECT-NON" | translate }} + + + {{ deviceModel.body.name }} + + +
+ +
+ {{ + "IOTDEVICE.CHANGE-APPLICATION.CHOOSE-DATA-TARGET" | translate + }} + + + + + + + + + +
+
+ + {{ "IOTDEVICE.CHANGE-APPLICATION.ADD-DATA-TARGET" | translate }} + + {{ + dataTarget.name + }} + + + +
+
+
+ + {{ "QUESTION.DATATARGET.SELECT-PAYLOADDECODER" | translate }} + + + {{ "QUESTION.DATATARGET.NO-PAYLOAD-DECODER-SELECTED" | translate }} + + + {{ payloadDecoder.name }} + + + +
+
+ +
+ +

{{ "DATATARGET.DELETE" | translate }}

+
+
+
+
+
+ +
+ + +
+
diff --git a/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.scss b/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.scss new file mode 100644 index 00000000..a628c840 --- /dev/null +++ b/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.scss @@ -0,0 +1,8 @@ +.iot-device-change-application-dialog { + width: 50vw; +} + +.mat-dialog-actions { + margin-left: 13px; + margin-bottom: 13px; +} diff --git a/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.ts b/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.ts new file mode 100644 index 00000000..bc69ff06 --- /dev/null +++ b/src/app/applications/iot-devices/iot-device-change-application-dialog/iot-device-change-application-dialog.component.ts @@ -0,0 +1,213 @@ +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { MatSnackBar } from "@angular/material/snack-bar"; +import { Organisation } from "@app/admin/organisation/organisation.model"; +import { OrganisationService } from "@app/admin/organisation/organisation.service"; +import { DeviceModelService } from "@app/device-model/device-model.service"; +import { DeviceModel } from "@app/device-model/device.model"; +import { Application } from "@applications/application.model"; +import { ApplicationService } from "@applications/application.service"; +import { TranslateService } from "@ngx-translate/core"; +import { IoTDeviceApplicationDialogModel } from "@shared/models/dialog.model"; +import { ReplaySubject } from "rxjs"; +import { IotDevice, UpdateIoTDeviceApplication } from "../iot-device.model"; +import { IoTDeviceService } from "../iot-device.service"; +import { PayloadDecoder, PayloadDecoderMappedResponse } from "@payload-decoder/payload-decoder.model"; +import { PayloadDecoderService } from "@payload-decoder/payload-decoder.service"; +import { DatatargetService } from "@applications/datatarget/datatarget.service"; +import { Datatarget } from "@applications/datatarget/datatarget.model"; +import { PayloadDeviceDatatargetService } from "@payload-decoder/payload-device-datatarget.service"; +import { faTimesCircle } from "@fortawesome/free-solid-svg-icons"; + +@Component({ + selector: "app-iot-device-change-application-dialog", + templateUrl: "./iot-device-change-application-dialog.component.html", + styleUrls: ["./iot-device-change-application-dialog.component.scss"], +}) +export class IoTDeviceChangeApplicationDialogComponent implements OnInit, OnDestroy { + public iotDevice: IotDevice; + public iotDeviceUpdate: UpdateIoTDeviceApplication; + public organizations: ReplaySubject = new ReplaySubject(1); + public applications: Application[]; + public filteredApplications: ReplaySubject = new ReplaySubject(1); + public deviceModels: DeviceModel[]; + public devices: IotDevice[] = []; + public payloadDecoders: PayloadDecoder[] = []; + public dataTargets: Datatarget[] = []; + faTimesCircle = faTimesCircle; + private subscriptions = []; + + constructor( + private iotDeviceService: IoTDeviceService, + private applicationService: ApplicationService, + public translate: TranslateService, + private organizationService: OrganisationService, + private deviceModelService: DeviceModelService, + private payloadDecoderService: PayloadDecoderService, + private dateTargetService: DatatargetService, + private payloadDeviceDatatargetService: PayloadDeviceDatatargetService, + private snackBar: MatSnackBar, + private dialog: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public dialogModel: IoTDeviceApplicationDialogModel + ) {} + + ngOnInit(): void { + this.translate.use("da"); + this.iotDeviceUpdate = { + deviceModelId: this.dialogModel.deviceId, + applicationId: 0, + organizationId: 0, + dataTargetToPayloadDecoderIds: [], + }; + + this.getIoTDeviceAndDefaultData(this.dialogModel.deviceId); + } + + ngOnDestroy(): void { + this.subscriptions.forEach(s => s?.unsubscribe()); + } + + getIoTDeviceAndDefaultData(id: number): void { + const iotDevicesSubscription = this.iotDeviceService.getIoTDevice(id).subscribe((iotDevice: IotDevice) => { + this.iotDevice = iotDevice; + this.getOrganizations(); + this.getApplications(); + this.getPayloadDecoders(); + + this.getDataTargets(this.iotDevice.application.id); + this.getDeviceModels(this.iotDevice.application.belongsTo.id); + + this.iotDeviceUpdate.deviceModelId = this.iotDevice.deviceModel.id; + this.iotDeviceUpdate.applicationId = this.iotDevice.application.id; + this.iotDeviceUpdate.organizationId = this.iotDevice.application.belongsTo.id; + + this.getIoTDeviceCurrentDataTargetsAndPayloadDecoders(this.dialogModel.deviceId); + }); + + this.subscriptions.push(iotDevicesSubscription); + } + + getIoTDeviceCurrentDataTargetsAndPayloadDecoders(id: number): void { + const payloadDeviceDatatargetSubscription = this.payloadDeviceDatatargetService + .getByIoTDevice(id) + .subscribe(dataTargetsAndPayloadDecoders => { + const dataTargetsAndPayloadIds = dataTargetsAndPayloadDecoders.data.map(val => { + return { dataTargetId: val.dataTarget.id, payloadDecoderId: val.payloadDecoder?.id }; + }); + this.iotDeviceUpdate.dataTargetToPayloadDecoderIds.push(...dataTargetsAndPayloadIds); + }); + + this.subscriptions.push(payloadDeviceDatatargetSubscription); + } + + getOrganizations() { + const organizationsSubscription = this.organizationService.getMultipleWithApplicationAdmin().subscribe(res => { + this.organizations.next(res.data.slice()); + }); + + this.subscriptions.push(organizationsSubscription); + } + + getApplications(): void { + const applicationsSubscription = this.applicationService + .getApplications(1000, 0, "asc", "id") + .subscribe(applicationData => { + this.applications = applicationData.data; + this.filteredApplications.next( + this.applications.filter(app => app.belongsTo.id === this.iotDevice.application.belongsTo.id) + ); + }); + + this.subscriptions.push(applicationsSubscription); + } + + getPayloadDecoders() { + const payloadDecoderSubscription = this.payloadDecoderService + .getMultiple(1000, 0, "id", "ASC") + .subscribe((response: PayloadDecoderMappedResponse) => { + this.payloadDecoders = response.data.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true })); + }); + + this.subscriptions.push(payloadDecoderSubscription); + } + + getDataTargets(applicationId: number) { + const dataTargetSubscription = this.dateTargetService + .getByApplicationId(1000, 0, applicationId) + .subscribe(response => { + this.dataTargets = response.data; + }); + + this.subscriptions.push(dataTargetSubscription); + } + + getDeviceModels(organizationId: number) { + const deviceModelSubscription = this.deviceModelService + .getMultiple(1000, 0, "asc", "id", organizationId) + .subscribe(res => { + this.deviceModels = res.data.sort((a, b) => a.body.name.localeCompare(b.body.name, "en", { numeric: true })); + }); + + this.subscriptions.push(deviceModelSubscription); + } + + public addRow() { + this.iotDeviceUpdate.dataTargetToPayloadDecoderIds.push({ + dataTargetId: null, + payloadDecoderId: null, + }); + } + + public deleteRow(index) { + this.iotDeviceUpdate.dataTargetToPayloadDecoderIds.splice(index, 1); + } + + onOrganizationChange() { + this.filteredApplications.next( + this.applications.filter(app => app.belongsTo.id === this.iotDeviceUpdate.organizationId) + ); + this.iotDeviceUpdate.applicationId = 0; + this.iotDeviceUpdate.deviceModelId = 0; + this.iotDeviceUpdate.dataTargetToPayloadDecoderIds = []; + this.dataTargets = []; + this.getDeviceModels(this.iotDeviceUpdate.organizationId); + } + + onApplicationChange() { + this.getDataTargets(this.iotDeviceUpdate.applicationId); + this.iotDeviceUpdate.dataTargetToPayloadDecoderIds = []; + } + + public compare(o1: any, o2: any): boolean { + return o1 === o2; + } + + onSubmit() { + if ( + !this.iotDeviceUpdate.applicationId || + !this.iotDeviceUpdate.organizationId || + this.iotDeviceUpdate.deviceModelId == null || + this.iotDeviceUpdate.dataTargetToPayloadDecoderIds.some(val => !val.dataTargetId) + ) + return; + + const iotDevicesSubscription = this.iotDeviceService + .changeIoTDeviceApplication(this.iotDevice.id, this.iotDeviceUpdate) + .subscribe(savedIoTDevice => { + this.snackBar.open( + this.translate.instant("IOTDEVICE.CHANGE-APPLICATION.SNACKBAR-SAVED", { + deviceName: savedIoTDevice.name, + applicationName: savedIoTDevice.application.name, + }), + this.translate.instant("DIALOG.OK"), + { + duration: 10000, + } + ); + this.dialog.close(true); + this.snackBar._openedSnackBarRef.afterDismissed().subscribe(() => location.reload()); + }); + + this.subscriptions.push(iotDevicesSubscription); + } +} 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 7c6d9a7e..483395ca 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 @@ -15,6 +15,8 @@ import { Title } from "@angular/platform-browser"; 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"; @Component({ selector: "app-iot-device", @@ -70,7 +72,8 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { private dialog: MatDialog, private titleService: Title, private meService: MeService, - private iotDeviceDetailsService: IotDeviceDetailsService + private iotDeviceDetailsService: IotDeviceDetailsService, + private changeApplicationDialog: MatDialog ) {} ngOnInit(): void { @@ -89,6 +92,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { label: "", editRouterLink: "../../iot-device-edit/" + this.deviceId, isErasable: true, + extraOptions: [], }; } @@ -115,7 +119,6 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { this.resetApiKeyCancel = translations["GEN.CANCEL"]; }); - this.dropdownButton.extraOptions = []; if (this.router.url.split("/").length <= 5) { this.router.navigateByUrl(this.router.url + "/details", { replaceUrl: true, @@ -136,6 +139,16 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { if (this.canEdit && this.device.type === DeviceType.GENERIC_HTTP) { this.dropdownButton.extraOptions.push(this.resetApiKeyOption); } + + if (this.device?.type !== DeviceType.SIGFOX) { + this.translate.get("IOTDEVICE.CHANGE-APPLICATION.TITLE").subscribe(translation => { + this.dropdownButton.extraOptions.push({ + id: this.deviceId, + label: translation, + onClick: () => this.onOpenChangeApplicationDialog(), + }); + }); + } }); } @@ -170,6 +183,14 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { } } + onOpenChangeApplicationDialog() { + this.changeApplicationDialog.open(IoTDeviceChangeApplicationDialogComponent, { + data: { + deviceId: this.deviceId, + } as IoTDeviceApplicationDialogModel, + }); + } + ngOnDestroy() { // prevent memory leak by unsubscribing if (this.iotDeviceSubscription) { diff --git a/src/app/applications/iot-devices/iot-device.model.ts b/src/app/applications/iot-devices/iot-device.model.ts index 4ac40664..c6977f0e 100644 --- a/src/app/applications/iot-devices/iot-device.model.ts +++ b/src/app/applications/iot-devices/iot-device.model.ts @@ -87,3 +87,10 @@ export class IoTDeviceStatsResponse { snr: number; rxPacketsPerDr?: Record; } + +export class UpdateIoTDeviceApplication { + public deviceModelId: number; + public organizationId: number; + public applicationId: number; + public dataTargetToPayloadDecoderIds: { dataTargetId: number; payloadDecoderId: number }[]; +} diff --git a/src/app/applications/iot-devices/iot-device.service.ts b/src/app/applications/iot-devices/iot-device.service.ts index a3f55e6d..82362803 100644 --- a/src/app/applications/iot-devices/iot-device.service.ts +++ b/src/app/applications/iot-devices/iot-device.service.ts @@ -6,6 +6,7 @@ import { IotDevicesImportResponse, IotDeviceImportRequest, IoTDeviceStatsResponse, + UpdateIoTDeviceApplication, } from "./iot-device.model"; import { RestService } from "src/app/shared/services/rest.service"; import { map } from "rxjs/operators"; @@ -66,6 +67,7 @@ export class IoTDeviceService { updatedBy: response.updatedBy, createdByName: this.userMinimalService.getUserNameFrom(response.createdBy), updatedByName: this.userMinimalService.getUserNameFrom(response.updatedBy), + deviceModel: response.deviceModel, }; }) ); @@ -90,4 +92,8 @@ export class IoTDeviceService { resetHttpDeviceApiKey(id: number): Observable> { return this.restService.put(`${this.BASEURL}/resetHttpDeviceApiKey`, null, id); } + + changeIoTDeviceApplication(id: number, body: UpdateIoTDeviceApplication): Observable { + return this.restService.put(`${this.BASEURL}/changeApplication`, body, id); + } } 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 a07be631..404ea847 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 @@ -142,6 +142,11 @@ >{{ "IOTDEVICE-TABLE-ROW.EDIT" | translate }} + diff --git a/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts b/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts index faba0d67..5eaa2e4a 100644 --- a/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts +++ b/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts @@ -18,6 +18,8 @@ import { IoTDeviceService } from "../iot-device.service"; import { DefaultPageSizeOptions } from "@shared/constants/page.constants"; import { ActivatedRoute } from "@angular/router"; import { TableColumn } from "@shared/types/table.type"; +import { IoTDeviceChangeApplicationDialogComponent } from "../iot-device-change-application-dialog/iot-device-change-application-dialog.component"; +import { IoTDeviceApplicationDialogModel } from "@shared/models/dialog.model"; const columnDefinitions: TableColumn[] = [ { @@ -113,16 +115,14 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { public pageSize = environment.tablePageSize; public pageSizeOptions = DefaultPageSizeOptions; public canEdit = false; - - private readonly CHIRPSTACK_BATTERY_NOT_AVAILIBLE = 255; - + deviceTypes = DeviceType; batteryStatusColor = "green"; resultsLength = 0; isLoadingResults = true; - displayedColumns: string[] = []; - iotDeviceSavedColumns = "iotDeviceSavedColumns"; + protected readonly columnDefinitions = columnDefinitions; + private readonly CHIRPSTACK_BATTERY_NOT_AVAILIBLE = 255; constructor( private restService: RestService, @@ -131,7 +131,8 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { public iotDeviceService: IoTDeviceService, private meService: MeService, private dialog: MatDialog, - private route: ActivatedRoute + private route: ActivatedRoute, + private changeApplicationDialog: MatDialog ) { translate.use("da"); moment.locale("da"); @@ -242,6 +243,14 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { } } + onOpenChangeApplicationDialog(id: number) { + this.changeApplicationDialog.open(IoTDeviceChangeApplicationDialogComponent, { + data: { + deviceId: id, + } as IoTDeviceApplicationDialogModel, + }); + } + showSigfoxDeleteDialog() { this.dialog.open(DeleteDialogComponent, { data: { @@ -260,6 +269,4 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { } return text.substring(0, maxLength) + "..."; } - - protected readonly columnDefinitions = columnDefinitions; } diff --git a/src/app/applications/iot-devices/iot-devices.module.ts b/src/app/applications/iot-devices/iot-devices.module.ts index b4782f72..fa6361a8 100644 --- a/src/app/applications/iot-devices/iot-devices.module.ts +++ b/src/app/applications/iot-devices/iot-devices.module.ts @@ -34,6 +34,7 @@ import { ExportCsvDialogComponent } from "./iot-devices-tab/export-csv-dialog/ex import { IotDeviceDetailsMqttInternalBrokerComponent } from "@applications/iot-devices/iot-device-detail/iot-device-details-mqtt-internal-broker/iot-device-details-mqtt-internal-broker.component"; import { IotDeviceDetailsMqttExternalBrokerComponent } from "@applications/iot-devices/iot-device-detail/iot-device-details-mqtt-external-broker/iot-device-details-mqtt-external-broker.component"; import { DownlinkTablesComponent } from "./iot-device-detail/downlink/downlink-tables/downlink-tables.component"; +import { IoTDeviceChangeApplicationDialogComponent } from "./iot-device-change-application-dialog/iot-device-change-application-dialog.component"; @NgModule({ declarations: [ @@ -60,6 +61,7 @@ import { DownlinkTablesComponent } from "./iot-device-detail/downlink/downlink-t MqttAuthenticationSelectComponent, IotDeviceDetailsMqttExternalBrokerComponent, ExportCsvDialogComponent, + IoTDeviceChangeApplicationDialogComponent, ], exports: [IotDevicesTableComponent, IoTDeviceDetailComponent], imports: [ diff --git a/src/app/device-model/device-model.service.ts b/src/app/device-model/device-model.service.ts index 1c0308af..b34227fd 100644 --- a/src/app/device-model/device-model.service.ts +++ b/src/app/device-model/device-model.service.ts @@ -78,7 +78,6 @@ export class DeviceModelService { offset: offset, sort: sort, orderOn: orderOn, - organizationId: undefined, }; if (organizationId) { body.organizationId = organizationId; @@ -86,25 +85,26 @@ export class DeviceModelService { return this.restService.get(this.DEVICEMODELURL, body).pipe( map(response => { return { - data: response.data.map( - (item: any) => - new DeviceModel( - item.id, - new DeviceModelBody( - item.body.id, - item.body.name, - item.body.brandName, - item.body.modelName, - item.body.manufacturerName, - item.body.category, - item.body.energyLimitationClass, - item.body.controlledProperty, - item.body.supportedUnits, - item.body.function, - item.body.supportedProtocol - ) + data: response.data.map((item: any) => { + let deviceModel = new DeviceModel( + item.id, + new DeviceModelBody( + item.body.id, + item.body.name, + item.body.brandName, + item.body.modelName, + item.body.manufacturerName, + item.body.category, + item.body.energyLimitationClass, + item.body.controlledProperty, + item.body.supportedUnits, + item.body.function, + item.body.supportedProtocol ) - ), + ); + deviceModel.belongsTo = item.belongsTo; + return deviceModel; + }), count: response.count, }; }) diff --git a/src/app/device-model/device.model.ts b/src/app/device-model/device.model.ts index 481343e0..faea8676 100644 --- a/src/app/device-model/device.model.ts +++ b/src/app/device-model/device.model.ts @@ -1,3 +1,5 @@ +import { Organisation } from "@app/admin/organisation/organisation.model"; + export class DeviceModelBody { id?: string; name?: string; @@ -48,6 +50,7 @@ export class DeviceModelResponse { export class DeviceModel { id: number; body: DeviceModelBody = new DeviceModelBody(); + belongsTo: Organisation; createdAt: string; updatedAt: string; createdBy: number; diff --git a/src/app/payload-decoder/payload-device-datatarget.service.ts b/src/app/payload-decoder/payload-device-datatarget.service.ts index 8f517c8e..9a51dc28 100644 --- a/src/app/payload-decoder/payload-device-datatarget.service.ts +++ b/src/app/payload-decoder/payload-device-datatarget.service.ts @@ -9,6 +9,7 @@ import { PayloadDeviceDatatarget, PayloadDeviceDatatargetGetByDataTargetResponse export class PayloadDeviceDatatargetService { private BASEURL = "iot-device-payload-decoder-data-target-connection"; private BYDATATARGETURL = "/byDataTarget"; + private BYDIOTDEVICEIDURL = "/byIoTDevice"; constructor(private restService: RestService) {} @@ -34,6 +35,10 @@ export class PayloadDeviceDatatargetService { return this.restService.get(this.BASEURL + this.BYDATATARGETURL, null, id); } + getByIoTDevice(id: number): Observable { + return this.restService.get(this.BASEURL + this.BYDIOTDEVICEIDURL, null, id); + } + mapToDatatargetDevicePayload(dto: PayloadDeviceDatatargetGetByDataTargetResponse): PayloadDeviceDatatarget[] { const payloadDeviceDatatargetList = []; dto.data.forEach(element => { diff --git a/src/app/shared/models/dialog.model.ts b/src/app/shared/models/dialog.model.ts index 4551ba36..cce4a56a 100644 --- a/src/app/shared/models/dialog.model.ts +++ b/src/app/shared/models/dialog.model.ts @@ -22,3 +22,7 @@ export class GatewayDialogModel { gatewayDbId: number; organizationId?: number; } + +export class IoTDeviceApplicationDialogModel { + deviceId: number; +} diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index d0132964..740b65e0 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -984,6 +984,15 @@ "COPY-TO-CLIPBOARD": "Kopier", "DOWNLOAD": "Download", "NOCONNECTION": "Denne enhed kunne ikke forbinde til den eksterne broker. Ret indstillingerne til eller slet enheden." + }, + "CHANGE-APPLICATION": { + "TITLE": "Skift applikation", + "CHOOSE-ORGANIZATION": "Vælg den organisation som ejer den applikationen, du vil skifte til", + "CHOOSE-APPLICATION": "Vælg hvilken applikation du vil skifte til", + "CHOOSE-DEVICE-MODEL": "Vælg device model", + "CHOOSE-DATA-TARGET": "Tilknyt data target", + "ADD-DATA-TARGET": "Vælg data target", + "SNACKBAR-SAVED": "{{deviceName}} er nu tilknyttet {{applicationName}}" } }, "MESSAGE": {