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 579955ee22..3a8d341cb4 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 @@ -94,6 +94,8 @@ import { DeviceProfileDataComponent } from './profile/device-profile-data.compon import { DeviceProfileComponent } from './profile/device-profile.component'; import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component'; 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'; @NgModule({ declarations: @@ -165,12 +167,14 @@ import { DeviceProfileTransportConfigurationComponent } from './profile/device/d TenantProfileDataComponent, TenantProfileComponent, TenantProfileDialogComponent, + DeviceProfileAutocompleteComponent, DefaultDeviceProfileConfigurationComponent, DeviceProfileConfigurationComponent, DefaultDeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent, DeviceProfileDataComponent, - DeviceProfileComponent + DeviceProfileComponent, + DeviceProfileDialogComponent ], imports: [ CommonModule, @@ -231,12 +235,14 @@ import { DeviceProfileTransportConfigurationComponent } from './profile/device/d TenantProfileDataComponent, TenantProfileComponent, TenantProfileDialogComponent, + DeviceProfileAutocompleteComponent, DefaultDeviceProfileConfigurationComponent, DeviceProfileConfigurationComponent, DefaultDeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent, DeviceProfileDataComponent, - DeviceProfileComponent + DeviceProfileComponent, + DeviceProfileDialogComponent ], providers: [ WidgetComponentService, 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 new file mode 100644 index 0000000000..7697059f94 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -0,0 +1,67 @@ + + + + + + + + + + +
+
+ device-profile.no-device-profiles-found +
+ + + {{ translate.get('device-profile.no-device-profiles-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + + device-profile.create-new-device-profile + +
+
+
+ + {{ 'device-profile.device-profile-required' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts new file mode 100644 index 0000000000..3ec583c85b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -0,0 +1,261 @@ +/// +/// 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, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { entityIdEquals } from '@shared/models/id/entity-id'; +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; +import { ENTER } from '@angular/cdk/keycodes'; +import { MatDialog } from '@angular/material/dialog'; +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileInfo, + DeviceProfileType, + DeviceTransportType +} from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; + +@Component({ + selector: 'tb-device-profile-autocomplete', + templateUrl: './device-profile-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), + multi: true + }] +}) +export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectDeviceProfileFormGroup: FormGroup; + + modelValue: DeviceProfileId | null; + + @Input() + selectDefaultProfile = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Output() + deviceProfileUpdated = new EventEmitter(); + + @Output() + deviceProfileChanged = new EventEmitter(); + + @ViewChild('deviceProfileInput', {static: true}) deviceProfileInput: ElementRef; + + filteredDeviceProfiles: Observable>; + + searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private deviceProfileService: DeviceProfileService, + private fb: FormBuilder, + private dialog: MatDialog) { + this.selectDeviceProfileFormGroup = this.fb.group({ + deviceProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredDeviceProfiles = this.selectDeviceProfileFormGroup.get('deviceProfile').valueChanges + .pipe( + tap((value: DeviceProfileInfo | string) => { + let modelValue: DeviceProfileInfo | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + this.updateView(modelValue); + }), + startWith(''), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchDeviceProfiles(name) ) + ); + } + + selectDefaultDeviceProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe( + (profile) => { + if (profile) { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); + this.updateView(profile); + } + } + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: DeviceProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new DeviceProfileId(profile.id.id); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); + } + ); + } else { + this.modelValue = null; + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); + this.selectDefaultDeviceProfileIfNeeded(); + } + } + + updateView(deviceProfile: DeviceProfileInfo | null) { + const idValue = deviceProfile ? new DeviceProfileId(deviceProfile.id.id) : null; + if (!entityIdEquals(this.modelValue, idValue)) { + this.modelValue = idValue; + this.propagateChange(this.modelValue); + this.deviceProfileChanged.emit(deviceProfile); + } + } + + displayDeviceProfileFn(profile?: DeviceProfileInfo): string | undefined { + return profile ? profile.name : undefined; + } + + fetchDeviceProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe( + map(pageData => { + return pageData.data; + }) + ); + } + + clear() { + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.deviceProfileInput.nativeElement.blur(); + this.deviceProfileInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + deviceProfileEnter($event: KeyboardEvent) { + if ($event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createDeviceProfile($event, this.searchText); + } + } + } + + createDeviceProfile($event: Event, profileName: string) { + $event.preventDefault(); + const deviceProfile: DeviceProfile = { + id: null, + name: profileName, + type: DeviceProfileType.DEFAULT, + transportType: DeviceTransportType.DEFAULT, + profileData: { + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), + transportConfiguration: createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT) + } + }; + this.openDeviceProfileDialog(deviceProfile, true); + } + + editDeviceProfile($event: Event) { + $event.preventDefault(); + this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe( + (deviceProfile) => { + this.openDeviceProfileDialog(deviceProfile, false); + } + ); + } + + openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { + this.dialog.open(DeviceProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + deviceProfile + } + }).afterClosed().subscribe( + (savedDeviceProfile) => { + if (!savedDeviceProfile) { + setTimeout(() => { + this.deviceProfileInput.nativeElement.blur(); + this.deviceProfileInput.nativeElement.focus(); + }, 0); + } else { + this.deviceProfileService.getDeviceProfileInfo(savedDeviceProfile.id.id).subscribe( + (profile) => { + this.modelValue = new DeviceProfileId(profile.id.id); + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); + if (isAdd) { + this.propagateChange(this.modelValue); + } else { + this.deviceProfileUpdated.next(savedDeviceProfile.id); + } + this.deviceProfileChanged.emit(profile); + } + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html new file mode 100644 index 0000000000..c55c973678 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts new file mode 100644 index 0000000000..b3e96c228d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.ts @@ -0,0 +1,101 @@ +/// +/// 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 { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { DeviceProfile } from '@shared/models/device.models'; +import { DeviceProfileComponent } from './device-profile.component'; +import { DeviceProfileService } from '@core/http/device-profile.service'; + +export interface DeviceProfileDialogData { + deviceProfile: DeviceProfile; + isAdd: boolean; +} + +@Component({ + selector: 'tb-device-profile-dialog', + templateUrl: './device-profile-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileDialogComponent}], + styleUrls: [] +}) +export class DeviceProfileDialogComponent extends + DialogComponent implements ErrorStateMatcher, AfterViewInit { + + isAdd: boolean; + deviceProfile: DeviceProfile; + + submitted = false; + + @ViewChild('deviceProfileComponent', {static: true}) deviceProfileComponent: DeviceProfileComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DeviceProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private deviceProfileService: DeviceProfileService) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.deviceProfile = this.data.deviceProfile; + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.deviceProfileComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.deviceProfileComponent.entityForm.valid) { + this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()}; + this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe( + (deviceProfile) => { + this.dialogRef.close(deviceProfile); + } + ); + } + } + +} 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 01ef989295..45e1492713 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 @@ -18,10 +18,10 @@
- - +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html new file mode 100644 index 0000000000..a1d4919b5c --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts new file mode 100644 index 0000000000..07d8d33b79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// 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 { + DefaultDeviceConfiguration, + DeviceConfiguration, + DeviceProfileType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-configuration', + templateUrl: './default-device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceConfigurationFormGroup: 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.defaultDeviceConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceConfiguration | null): void { + this.defaultDeviceConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceConfiguration = null; + if (this.defaultDeviceConfigurationFormGroup.valid) { + configuration = this.defaultDeviceConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceProfileType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html new file mode 100644 index 0000000000..cd2d53aab0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts new file mode 100644 index 0000000000..111c632a74 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts @@ -0,0 +1,97 @@ +/// +/// 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 { + DefaultDeviceTransportConfiguration, + DeviceTransportConfiguration, + DeviceTransportType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-transport-configuration', + templateUrl: './default-device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceTransportConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceTransportConfigurationFormGroup: 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.defaultDeviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceTransportConfiguration | null): void { + this.defaultDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.defaultDeviceTransportConfigurationFormGroup.valid) { + configuration = this.defaultDeviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceTransportType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html new file mode 100644 index 0000000000..ffda7bac44 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts new file mode 100644 index 0000000000..3e6abc1fd6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-configuration.component.ts @@ -0,0 +1,103 @@ +/// +/// 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 { DeviceConfiguration, DeviceProfileType } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-configuration', + templateUrl: './device-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceConfigurationComponent), + multi: true + }] +}) +export class DeviceConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceProfileType = DeviceProfileType; + + deviceConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + type: DeviceProfileType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceConfiguration = null; + if (this.deviceConfigurationFormGroup.valid) { + configuration = this.deviceConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} 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 new file mode 100644 index 0000000000..ad143091e6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.html @@ -0,0 +1,43 @@ + +
+ + + + +
device.device-configuration
+
+
+ + +
+ + + +
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 new file mode 100644 index 0000000000..77bea4a9e5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-data.component.ts @@ -0,0 +1,94 @@ +/// +/// 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 { DeviceData } from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-data', + templateUrl: './device-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceDataComponent), + multi: true + }] +}) +export class DeviceDataComponent implements ControlValueAccessor, OnInit { + + deviceDataFormGroup: 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.deviceDataFormGroup = this.fb.group({ + configuration: [null, Validators.required], + transportConfiguration: [null, Validators.required] + }); + this.deviceDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceDataFormGroup.disable({emitEvent: false}); + } else { + this.deviceDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceData | null): void { + this.deviceDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); + } + + private updateModel() { + let deviceData: DeviceData = null; + if (this.deviceDataFormGroup.valid) { + deviceData = this.deviceDataFormGroup.getRawValue(); + } + this.propagateChange(deviceData); + } +} 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 new file mode 100644 index 0000000000..3f9c73083b --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts new file mode 100644 index 0000000000..505f494551 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts @@ -0,0 +1,106 @@ +/// +/// 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 +} from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; + +@Component({ + selector: 'tb-device-transport-configuration', + templateUrl: './device-transport-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), + multi: true + }] +}) +export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceTransportType = DeviceTransportType; + + deviceTransportConfigurationFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + transportType: DeviceTransportType; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceTransportConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceTransportConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceTransportConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceTransportConfiguration | null): void { + this.transportType = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceTransportConfiguration = null; + if (this.deviceTransportConfigurationFormGroup.valid) { + configuration = this.deviceTransportConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.transportType; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.html b/ui-ngx/src/app/modules/home/pages/device/device.component.html index 72a8f153b1..16a38d9755 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.html @@ -83,6 +83,13 @@ {{ 'device.name-required' | translate }} + + device.label + +
{{ 'device.is-gateway' | translate }} 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 53a3eaa3db..cce82dca0f 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 @@ -19,7 +19,16 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { EntityComponent } from '../../components/entity/entity.component'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { DeviceInfo } from '@shared/models/device.models'; +import { + createDeviceConfiguration, + createDeviceProfileConfiguration, createDeviceTransportConfiguration, + DeviceData, + DeviceInfo, + DeviceProfileData, + DeviceProfileInfo, + DeviceProfileType, + DeviceTransportType +} from '@shared/models/device.models'; import { EntityType } from '@shared/models/entity-type.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { ActionNotificationShow } from '@core/notification/notification.actions'; @@ -126,4 +135,36 @@ export class DeviceComponent extends EntityComponent { ); } } + + onDeviceProfileUpdated() { + this.entitiesTableConfig.table.updateData(false); + } + + onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) { + if (deviceProfile) { + const deviceProfileType: DeviceProfileType = deviceProfile.type; + const deviceTransportType: DeviceTransportType = deviceProfile.transportType; + let deviceData: DeviceData = this.entityForm.getRawValue().deviceData; + if (!deviceData) { + deviceData = { + configuration: createDeviceConfiguration(deviceProfileType), + transportConfiguration: createDeviceTransportConfiguration(deviceTransportType) + }; + this.entityForm.patchValue({deviceData}); + } else { + let changed = false; + if (deviceData.configuration.type !== deviceProfileType) { + deviceData.configuration = createDeviceConfiguration(deviceProfileType); + changed = true; + } + if (deviceData.transportConfiguration.type !== deviceTransportType) { + deviceData.transportConfiguration = createDeviceTransportConfiguration(deviceTransportType); + changed = true; + } + if (changed) { + this.entityForm.patchValue({deviceData}); + } + } + } + } } 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 c6e7c3bcc4..782b92322f 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 @@ -24,9 +24,19 @@ import { DeviceCredentialsDialogComponent } from '@modules/home/pages/device/dev import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; +import { DefaultDeviceConfigurationComponent } from './data/default-device-configuration.component'; +import { DeviceConfigurationComponent } from './data/device-configuration.component'; +import { DeviceDataComponent } from './data/device-data.component'; +import { DefaultDeviceTransportConfigurationComponent } from './data/default-device-transport-configuration.component'; +import { DeviceTransportConfigurationComponent } from './data/device-transport-configuration.component'; @NgModule({ declarations: [ + DefaultDeviceConfigurationComponent, + DeviceConfigurationComponent, + DefaultDeviceTransportConfigurationComponent, + DeviceTransportConfigurationComponent, + DeviceDataComponent, DeviceComponent, DeviceTabsComponent, DeviceTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index d2bef0da4f..158230c659 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -86,6 +86,8 @@ export class DevicesTableConfigResolver implements Resolve this.translate.instant('device.delete-device-title', { deviceName: device.name }); this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index f393042dd0..3b80fd03ec 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -91,6 +91,19 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic return configuration; } +export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { + let configuration: DeviceConfiguration = null; + if (type) { + switch (type) { + case DeviceProfileType.DEFAULT: + const defaultConfiguration: DefaultDeviceConfiguration = {}; + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; + break; + } + } + return configuration; +} + export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { let transportConfiguration: DeviceProfileTransportConfiguration = null; if (type) { @@ -112,6 +125,27 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT return transportConfiguration; } +export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { + let transportConfiguration: DeviceTransportConfiguration = null; + if (type) { + switch (type) { + case DeviceTransportType.DEFAULT: + const defaultTransportConfiguration: DefaultDeviceTransportConfiguration = {}; + transportConfiguration = {...defaultTransportConfiguration, type: DeviceTransportType.DEFAULT}; + break; + case DeviceTransportType.MQTT: + const mqttTransportConfiguration: MqttDeviceTransportConfiguration = {}; + transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; + break; + case DeviceTransportType.LWM2M: + const lwm2mTransportConfiguration: Lwm2mDeviceTransportConfiguration = {}; + transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; + break; + } + } + return transportConfiguration; +} + export interface DeviceProfileData { configuration: DeviceProfileConfiguration; transportConfiguration: DeviceProfileTransportConfiguration; @@ -121,7 +155,7 @@ export interface DeviceProfile extends BaseData { tenantId?: TenantId; name: string; description?: string; - default: boolean; + default?: boolean; type: DeviceProfileType; transportType: DeviceTransportType; defaultRuleChainId?: RuleChainId; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e6895befc0..8dbfb985b3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -748,7 +748,9 @@ "import": "Import device", "device-file": "Device file", "search": "Search devices", - "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected" + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected", + "device-configuration": "Device configuration", + "transport-configuration": "Transport configuration" }, "device-profile": { "device-profile": "Device profile",