diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index 51f305c988..5db29e7004 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -237,21 +237,6 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { .andExpect(status().isNotFound()); } - @Test - public void testSaveSameDeviceWithDifferentDeviceProfileId() throws Exception { - Device device = new Device(); - device.setName("My device"); - device.setType("default"); - Device savedDevice = doPost("/api/device", device, Device.class); - DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile 2"); - DeviceProfile savedDeviceProfile2 = doPost("/api/deviceProfile", deviceProfile2, DeviceProfile.class); - - savedDevice.setDeviceProfileId(savedDeviceProfile2.getId()); - - doPost("/api/device/", savedDevice).andExpect(status().isBadRequest()) - .andExpect(statusReason(containsString("Changing device profile is prohibited"))); - } - @Test public void testAssignDeviceToCustomerFromDifferentTenant() throws Exception { loginSysAdmin(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java index 1c6022e12f..e37ef14933 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/Lwm2mDeviceTransportConfiguration.java @@ -15,13 +15,32 @@ */ package org.thingsboard.server.common.data.device.data; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import java.util.HashMap; +import java.util.Map; + @Data public class Lwm2mDeviceTransportConfiguration implements DeviceTransportConfiguration { + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + @Override public DeviceTransportType getType() { return DeviceTransportType.LWM2M; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java index 6cbdee4a65..3d27193ae8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/MqttDeviceTransportConfiguration.java @@ -15,12 +15,31 @@ */ package org.thingsboard.server.common.data.device.data; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +import java.util.HashMap; +import java.util.Map; + @Data public class MqttDeviceTransportConfiguration implements DeviceTransportConfiguration { + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + @Override public DeviceTransportType getType() { return DeviceTransportType.MQTT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java index 83e1247e1c..b2bdd63009 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java @@ -15,13 +15,32 @@ */ package org.thingsboard.server.common.data.device.profile; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import java.util.HashMap; +import java.util.Map; + @Data public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + @Override public DeviceTransportType getType() { return DeviceTransportType.LWM2M; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index 6a65d70b43..7b79608f3f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -15,12 +15,31 @@ */ package org.thingsboard.server.common.data.device.profile; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +import java.util.HashMap; +import java.util.Map; + @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { + @JsonIgnore + private Map properties = new HashMap<>(); + + @JsonAnyGetter + public Map properties() { + return this.properties; + } + + @JsonAnySetter + public void put(String name, Object value) { + this.properties.put(name, value); + } + @Override public DeviceTransportType getType() { return DeviceTransportType.MQTT; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 7f43472293..7153069849 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -436,8 +436,6 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe Device old = deviceDao.findById(device.getTenantId(), device.getId().getId()); if (old == null) { throw new DataValidationException("Can't update non existing device!"); - } else if (!old.getDeviceProfileId().equals(device.getDeviceProfileId())) { - throw new DataValidationException("Changing device profile is prohibited!"); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index 29da0e7f87..3804361e29 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -128,23 +128,6 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { } } - @Test(expected = DataValidationException.class) - public void testSaveSameDeviceWithDifferentDeviceProfileId() { - Device device = new Device(); - device.setName("My device"); - device.setType("default"); - device.setTenantId(tenantId); - device = deviceService.saveDevice(device); - DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId,"Device Profile 2"); - DeviceProfile savedDeviceProfile2 = deviceProfileService.saveDeviceProfile(deviceProfile2); - device.setDeviceProfileId(savedDeviceProfile2.getId()); - try { - deviceService.saveDevice(device); - } finally { - deviceService.deleteDevice(tenantId, device.getId()); - } - } - @Test(expected = DataValidationException.class) public void testAssignDeviceToCustomerFromDifferentTenant() { Device device = new Device(); diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 35a1331060..4440d8a4a9 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/ui-ngx/src/app/core/http/widget.service.ts @@ -43,6 +43,8 @@ export class WidgetService { private systemWidgetsBundles: Array; private tenantWidgetsBundles: Array; + private loadWidgetsBundleCacheSubject: ReplaySubject; + constructor( private http: HttpClient, private utils: UtilsService, @@ -238,34 +240,36 @@ export class WidgetService { private loadWidgetsBundleCache(config?: RequestConfig): Observable { if (!this.allWidgetsBundles) { - const loadWidgetsBundleCacheSubject = new ReplaySubject(); - this.http.get>('/api/widgetsBundles', - defaultHttpOptionsFromConfig(config)).subscribe( - (allWidgetsBundles) => { - this.allWidgetsBundles = allWidgetsBundles; - this.systemWidgetsBundles = new Array(); - this.tenantWidgetsBundles = new Array(); - this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { - let res = wb1.title.localeCompare(wb2.title); - if (res === 0) { - res = wb2.createdTime - wb1.createdTime; - } - return res; - }); - this.allWidgetsBundles.forEach((widgetsBundle) => { - if (widgetsBundle.tenantId.id === NULL_UUID) { - this.systemWidgetsBundles.push(widgetsBundle); - } else { - this.tenantWidgetsBundles.push(widgetsBundle); - } + if (!this.loadWidgetsBundleCacheSubject) { + this.loadWidgetsBundleCacheSubject = new ReplaySubject(); + this.http.get>('/api/widgetsBundles', + defaultHttpOptionsFromConfig(config)).subscribe( + (allWidgetsBundles) => { + this.allWidgetsBundles = allWidgetsBundles; + this.systemWidgetsBundles = new Array(); + this.tenantWidgetsBundles = new Array(); + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { + let res = wb1.title.localeCompare(wb2.title); + if (res === 0) { + res = wb2.createdTime - wb1.createdTime; + } + return res; + }); + this.allWidgetsBundles.forEach((widgetsBundle) => { + if (widgetsBundle.tenantId.id === NULL_UUID) { + this.systemWidgetsBundles.push(widgetsBundle); + } else { + this.tenantWidgetsBundles.push(widgetsBundle); + } + }); + this.loadWidgetsBundleCacheSubject.next(); + this.loadWidgetsBundleCacheSubject.complete(); + }, + () => { + this.loadWidgetsBundleCacheSubject.error(null); }); - loadWidgetsBundleCacheSubject.next(); - loadWidgetsBundleCacheSubject.complete(); - }, - () => { - loadWidgetsBundleCacheSubject.error(null); - }); - return loadWidgetsBundleCacheSubject.asObservable(); + } + return this.loadWidgetsBundleCacheSubject.asObservable(); } else { return of(null); } @@ -275,6 +279,7 @@ export class WidgetService { this.allWidgetsBundles = undefined; this.systemWidgetsBundles = undefined; this.tenantWidgetsBundles = undefined; + this.loadWidgetsBundleCacheSubject = undefined; } } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 3a8d341cb4..8e779a8106 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -96,6 +96,8 @@ import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/d import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component'; import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.component'; import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component'; +import { MqttDeviceProfileTransportConfigurationComponent } from './profile/device/mqtt-device-profile-transport-configuration.component'; +import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component'; @NgModule({ declarations: @@ -171,6 +173,8 @@ import { DeviceProfileAutocompleteComponent } from './profile/device-profile-aut DefaultDeviceProfileConfigurationComponent, DeviceProfileConfigurationComponent, DefaultDeviceProfileTransportConfigurationComponent, + MqttDeviceProfileTransportConfigurationComponent, + Lwm2mDeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent, DeviceProfileDataComponent, DeviceProfileComponent, @@ -239,6 +243,8 @@ import { DeviceProfileAutocompleteComponent } from './profile/device-profile-aut DefaultDeviceProfileConfigurationComponent, DeviceProfileConfigurationComponent, DefaultDeviceProfileTransportConfigurationComponent, + MqttDeviceProfileTransportConfigurationComponent, + Lwm2mDeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent, DeviceProfileDataComponent, DeviceProfileComponent, diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html index 7697059f94..38ee98a895 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -19,6 +19,7 @@ { }; constructor(private store: Store, @@ -115,9 +117,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, } this.updateView(modelValue); }), - startWith(''), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), - mergeMap(name => this.fetchDeviceProfiles(name) ) + mergeMap(name => this.fetchDeviceProfiles(name) ), + share() ); } @@ -144,14 +146,23 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( (profile) => { this.modelValue = new DeviceProfileId(profile.id.id); - this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); + this.deviceProfileChanged.emit(profile); } ); } else { this.modelValue = null; - this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: false}); this.selectDefaultDeviceProfileIfNeeded(); } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectDeviceProfileFormGroup.get('deviceProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } } updateView(deviceProfile: DeviceProfileInfo | null) { diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html index 483c9f9282..df58cb831c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html @@ -17,7 +17,7 @@ -->
- +
device-profile.profile-configuration
@@ -28,7 +28,7 @@ required>
- +
device-profile.transport-configuration
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts index 955c32efdd..01da29d444 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts @@ -19,7 +19,12 @@ import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Valida import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { DeviceProfileData } from '@shared/models/device.models'; +import { + DeviceProfileData, + DeviceProfileType, + deviceProfileTypeConfigurationInfoMap, + DeviceTransportType, deviceTransportTypeConfigurationInfoMap +} from '@shared/models/device.models'; @Component({ selector: 'tb-device-profile-data', @@ -47,6 +52,9 @@ export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit @Input() disabled: boolean; + displayProfileConfiguration: boolean; + displayTransportConfiguration: boolean; + private propagateChange = (v: any) => { }; constructor(private store: Store, @@ -80,6 +88,12 @@ export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit } writeValue(value: DeviceProfileData | null): void { + const deviceProfileType = value?.configuration?.type; + this.displayProfileConfiguration = deviceProfileType && + deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasProfileConfiguration; + const deviceTransportType = value?.transportConfiguration?.type; + this.displayTransportConfiguration = deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration; this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); this.deviceProfileDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html index 45e1492713..001502cd83 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html @@ -23,5 +23,17 @@ formControlName="configuration"> + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..03530c2b2e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..ed81a143fd --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m-device-profile-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceProfileTransportConfiguration, + DeviceTransportType, Lwm2mDeviceProfileTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-lwm2m-device-profile-transport-configuration', + templateUrl: './lwm2m-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class Lwm2mDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + lwm2mDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.lwm2mDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.lwm2mDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Lwm2mDeviceProfileTransportConfiguration | null): void { + this.lwm2mDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.lwm2mDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.lwm2mDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.LWM2M; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html new file mode 100644 index 0000000000..5dbb46c8bf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts new file mode 100644 index 0000000000..1cb70ce6da --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceProfileTransportConfiguration, + DeviceTransportType, MqttDeviceProfileTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-mqtt-device-profile-transport-configuration', + templateUrl: './mqtt-device-profile-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceProfileTransportConfigurationComponent), + multi: true + }] +}) +export class MqttDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + mqttDeviceProfileTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.mqttDeviceProfileTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.mqttDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.mqttDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.mqttDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MqttDeviceProfileTransportConfiguration | null): void { + this.mqttDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileTransportConfiguration = null; + if (this.mqttDeviceProfileTransportConfigurationFormGroup.valid) { + configuration = this.mqttDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.MQTT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html index 5a81b2b20f..9dd73c5efe 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.html @@ -19,6 +19,7 @@ { }; constructor(private store: Store, @@ -106,9 +108,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, } this.updateView(modelValue); }), - startWith(''), map(value => value ? (typeof value === 'string' ? value : value.name) : ''), - mergeMap(name => this.fetchTenantProfiles(name) ) + mergeMap(name => this.fetchTenantProfiles(name) ), + share() ); } @@ -136,14 +138,22 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( (profile) => { this.modelValue = new TenantProfileId(profile.id.id); - this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: true}); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); } ); } else { this.modelValue = null; - this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: true}); + this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(null, {emitEvent: false}); this.selectDefaultTenantProfileIfNeeded(); } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectTenantProfileFormGroup.get('tenantProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } } updateView(value: TenantProfileId | null) { diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html index ad143091e6..0cff6b3693 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html @@ -17,7 +17,7 @@ -->
- +
device.device-configuration
@@ -28,7 +28,7 @@ required>
- +
device.transport-configuration
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts index 77bea4a9e5..db9297f275 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts @@ -19,7 +19,11 @@ import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Valida import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { DeviceData } from '@shared/models/device.models'; +import { + DeviceData, + deviceProfileTypeConfigurationInfoMap, + deviceTransportTypeConfigurationInfoMap +} from '@shared/models/device.models'; @Component({ selector: 'tb-device-data', @@ -47,6 +51,9 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit { @Input() disabled: boolean; + displayDeviceConfiguration: boolean; + displayTransportConfiguration: boolean; + private propagateChange = (v: any) => { }; constructor(private store: Store, @@ -80,6 +87,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit { } writeValue(value: DeviceData | null): void { + const deviceProfileType = value?.configuration?.type; + this.displayDeviceConfiguration = deviceProfileType && + deviceProfileTypeConfigurationInfoMap.get(deviceProfileType).hasDeviceConfiguration; + const deviceTransportType = value?.transportConfiguration?.type; + this.displayTransportConfiguration = deviceTransportType && + deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasDeviceConfiguration; this.deviceDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); } diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html index 3f9c73083b..f109335edc 100644 --- a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html @@ -23,5 +23,17 @@ formControlName="configuration"> + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html new file mode 100644 index 0000000000..3fdccba628 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts new file mode 100644 index 0000000000..05e1448bc3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/lwm2m-device-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType, Lwm2mDeviceTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-lwm2m-device-transport-configuration', + templateUrl: './lwm2m-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => Lwm2mDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class Lwm2mDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + lwm2mDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.lwm2mDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.lwm2mDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.lwm2mDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.lwm2mDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: Lwm2mDeviceTransportConfiguration | null): void { + this.lwm2mDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.lwm2mDeviceTransportConfigurationFormGroup.valid) { + configuration = this.lwm2mDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.LWM2M; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html new file mode 100644 index 0000000000..e21bb3818a --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts new file mode 100644 index 0000000000..68348b8017 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/mqtt-device-transport-configuration.component.ts @@ -0,0 +1,96 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { + DeviceTransportConfiguration, + DeviceTransportType, MqttDeviceTransportConfiguration +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-mqtt-device-transport-configuration', + templateUrl: './mqtt-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => MqttDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class MqttDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + mqttDeviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.mqttDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.mqttDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.mqttDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.mqttDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: MqttDeviceTransportConfiguration | null): void { + this.mqttDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.mqttDeviceTransportConfigurationFormGroup.valid) { + configuration = this.mqttDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.MQTT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index cce82dca0f..1dfee29a61 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -141,7 +141,7 @@ export class DeviceComponent extends EntityComponent { } onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) { - if (deviceProfile) { + if (deviceProfile && this.isEdit) { const deviceProfileType: DeviceProfileType = deviceProfile.type; const deviceTransportType: DeviceTransportType = deviceProfile.transportType; let deviceData: DeviceData = this.entityForm.getRawValue().deviceData; @@ -151,6 +151,7 @@ export class DeviceComponent extends EntityComponent { transportConfiguration: createDeviceTransportConfiguration(deviceTransportType) }; this.entityForm.patchValue({deviceData}); + this.entityForm.markAsDirty(); } else { let changed = false; if (deviceData.configuration.type !== deviceProfileType) { @@ -163,6 +164,7 @@ export class DeviceComponent extends EntityComponent { } if (changed) { this.entityForm.patchValue({deviceData}); + this.entityForm.markAsDirty(); } } } diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts index 782b92322f..53ee34d570 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -29,12 +29,16 @@ import { DeviceConfigurationComponent } from './data/device-configuration.compon import { DeviceDataComponent } from './data/device-data.component'; import { DefaultDeviceTransportConfigurationComponent } from './data/default-device-transport-configuration.component'; import { DeviceTransportConfigurationComponent } from './data/device-transport-configuration.component'; +import { MqttDeviceTransportConfigurationComponent } from './data/mqtt-device-transport-configuration.component'; +import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-transport-configuration.component'; @NgModule({ declarations: [ DefaultDeviceConfigurationComponent, DeviceConfigurationComponent, DefaultDeviceTransportConfigurationComponent, + MqttDeviceTransportConfigurationComponent, + Lwm2mDeviceTransportConfigurationComponent, DeviceTransportConfigurationComponent, DeviceDataComponent, DeviceComponent, diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 3b80fd03ec..90781726d7 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -34,12 +34,29 @@ export enum DeviceTransportType { LWM2M = 'LWM2M' } +export interface DeviceConfigurationFormInfo { + hasProfileConfiguration: boolean; + hasDeviceConfiguration: boolean; +} + export const deviceProfileTypeTranslationMap = new Map( [ [DeviceProfileType.DEFAULT, 'device-profile.type-default'] ] ); +export const deviceProfileTypeConfigurationInfoMap = new Map( + [ + [ + DeviceProfileType.DEFAULT, + { + hasProfileConfiguration: false, + hasDeviceConfiguration: false, + } + ] + ] +); + export const deviceTransportTypeTranslationMap = new Map( [ [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default'], @@ -48,6 +65,32 @@ export const deviceTransportTypeTranslationMap = new Map( + [ + [ + DeviceTransportType.DEFAULT, + { + hasProfileConfiguration: false, + hasDeviceConfiguration: false, + } + ], + [ + DeviceTransportType.MQTT, + { + hasProfileConfiguration: true, + hasDeviceConfiguration: true, + } + ], + [ + DeviceTransportType.LWM2M, + { + hasProfileConfiguration: true, + hasDeviceConfiguration: true, + } + ] + ] +); + export interface DefaultDeviceProfileConfiguration { [key: string]: any; }