diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts new file mode 100644 index 0000000000..8cdc8c188b --- /dev/null +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -0,0 +1,66 @@ +/// +/// 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { PageData } from '@shared/models/page/page-data'; +import { DeviceProfile, DeviceProfileInfo } from '@shared/models/device.models'; + +@Injectable({ + providedIn: 'root' +}) +export class DeviceProfileService { + + constructor( + private http: HttpClient + ) { } + + public getDeviceProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable { + return this.http.post('/api/deviceProfile', deviceProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteDeviceProfile(deviceProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/deviceProfile/${deviceProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultDeviceProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/deviceProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileInfo(deviceProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index efa72f2951..2691fc50bb 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -215,6 +215,13 @@ export class MenuService { path: '/devices', icon: 'devices_other' }, + { + name: 'device-profile.device-profiles', + type: 'link', + path: '/deviceProfiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true + }, { name: 'entity-view.entity-views', type: 'link', @@ -283,6 +290,12 @@ export class MenuService { name: 'device.devices', icon: 'devices_other', path: '/devices' + }, + { + name: 'device-profile.device-profiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true, + path: '/deviceProfiles' } ] }, 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 40b64e2fc9..a53c2f69b8 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 @@ -88,6 +88,10 @@ import { TenantProfileAutocompleteComponent } from './profile/tenant-profile-aut import { TenantProfileComponent } from './profile/tenant-profile.component'; import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component'; import { TenantProfileDataComponent } from './profile/tenant-profile-data.component'; +import { DefaultDeviceProfileConfigurationComponent } from './profile/device/default-device-profile-configuration.component'; +import { DeviceProfileConfigurationComponent } from './profile/device/device-profile-configuration.component'; +import { DeviceProfileDataComponent } from './profile/device-profile-data.component'; +import { DeviceProfileComponent } from './profile/device-profile.component'; @NgModule({ declarations: @@ -158,7 +162,11 @@ import { TenantProfileDataComponent } from './profile/tenant-profile-data.compon TenantProfileAutocompleteComponent, TenantProfileDataComponent, TenantProfileComponent, - TenantProfileDialogComponent + TenantProfileDialogComponent, + DefaultDeviceProfileConfigurationComponent, + DeviceProfileConfigurationComponent, + DeviceProfileDataComponent, + DeviceProfileComponent ], imports: [ CommonModule, @@ -218,7 +226,11 @@ import { TenantProfileDataComponent } from './profile/tenant-profile-data.compon TenantProfileAutocompleteComponent, TenantProfileDataComponent, TenantProfileComponent, - TenantProfileDialogComponent + TenantProfileDialogComponent, + DefaultDeviceProfileConfigurationComponent, + DeviceProfileConfigurationComponent, + DeviceProfileDataComponent, + DeviceProfileComponent ], providers: [ WidgetComponentService, 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 new file mode 100644 index 0000000000..4666a31612 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html @@ -0,0 +1,40 @@ + +
+ + + + +
device-profile.profile-configuration
+
+
+ + +
+ + + +
device-profile.transport-configuration
+
+
+ TODO +
+
+
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 new file mode 100644 index 0000000000..08a5a1196c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts @@ -0,0 +1,92 @@ +/// +/// 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 { DeviceProfileData } from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-profile-data', + templateUrl: './device-profile-data.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileDataComponent), + multi: true + }] +}) +export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit { + + deviceProfileDataFormGroup: 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.deviceProfileDataFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceProfileDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileDataFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileDataFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileData | null): void { + this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); + } + + private updateModel() { + let deviceProfileData: DeviceProfileData = null; + if (this.deviceProfileDataFormGroup.valid) { + deviceProfileData = this.deviceProfileDataFormGroup.getRawValue(); + } + this.propagateChange(deviceProfileData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html new file mode 100644 index 0000000000..7b8742cecc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -0,0 +1,77 @@ + +
+ + +
+ +
+
+
+
+
+ + device-profile.name + + + {{ 'device-profile.name-required' | translate }} + + + + device-profile.type + + + {{deviceProfileTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.type-required' | translate }} + + + + + + + + tenant-profile.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts new file mode 100644 index 0000000000..0d3f286b3f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts @@ -0,0 +1,128 @@ +/// +/// 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, Inject, Input, Optional } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityComponent } from '../entity/entity.component'; +import { + createDeviceProfileConfiguration, + DeviceProfile, + DeviceProfileData, + DeviceProfileType, + deviceProfileTypeTranslationMap +} from '@shared/models/device.models'; +import { EntityType } from '@shared/models/entity-type.models'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; + +@Component({ + selector: 'tb-device-profile', + templateUrl: './device-profile.component.html', + styleUrls: [] +}) +export class DeviceProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + entityType = EntityType; + + deviceProfileTypes = Object.keys(DeviceProfileType); + + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap; + + constructor(protected store: Store, + protected translate: TranslateService, + @Optional() @Inject('entity') protected entityValue: DeviceProfile, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: DeviceProfile): FormGroup { + const form = this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + type: [entity ? entity.type : '', [Validators.required]], + profileData: [entity && !this.isAdd ? entity.profileData : {}, []], + defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], + description: [entity ? entity.description : '', []], + } + ); + form.get('type').valueChanges.subscribe(() => { + this.deviceProfileTypeChanged(form); + }); + this.checkIsNewDeviceProfile(entity, form); + return form; + } + + private checkIsNewDeviceProfile(entity: DeviceProfile, form: FormGroup) { + if (entity && !entity.id) { + form.get('type').patchValue(DeviceProfileType.DEFAULT, {emitEvent: true}); + } + } + + private deviceProfileTypeChanged(form: FormGroup) { + const deviceProfileType: DeviceProfileType = form.get('type').value; + let profileData: DeviceProfileData = form.getRawValue().profileData; + if (!profileData) { + profileData = { + configuration: null + }; + } + profileData.configuration = createDeviceProfileConfiguration(deviceProfileType); + this.entityForm.patchValue({profileData}); + } + + updateForm(entity: DeviceProfile) { + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({type: entity.type}); + this.entityForm.patchValue({profileData: entity.profileData}); + this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}); + this.entityForm.patchValue({description: entity.description}); + } + + prepareFormValue(formValue: any): any { + if (formValue.defaultRuleChainId) { + formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId); + } + return formValue; + } + + onDeviceProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html new file mode 100644 index 0000000000..200d3d3623 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html @@ -0,0 +1,24 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts new file mode 100644 index 0000000000..211cd5ada2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-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 { + DefaultDeviceProfileConfiguration, + DeviceProfileConfiguration, + DeviceProfileType +} from '@shared/models/device.models'; + +@Component({ + selector: 'tb-default-device-profile-configuration', + templateUrl: './default-device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DefaultDeviceProfileConfigurationComponent), + multi: true + }] +}) +export class DefaultDeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + defaultDeviceProfileConfigurationFormGroup: 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.defaultDeviceProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.defaultDeviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.defaultDeviceProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.defaultDeviceProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DefaultDeviceProfileConfiguration | null): void { + this.defaultDeviceProfileConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileConfiguration = null; + if (this.defaultDeviceProfileConfigurationFormGroup.valid) { + configuration = this.defaultDeviceProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = DeviceProfileType.DEFAULT; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html new file mode 100644 index 0000000000..3b7879b933 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html @@ -0,0 +1,27 @@ + +
+
+ + + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts new file mode 100644 index 0000000000..0185639fa0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-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 { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models'; +import { deepClone } from '../../../../../core/utils'; + +@Component({ + selector: 'tb-device-profile-configuration', + templateUrl: './device-profile-configuration.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileConfigurationComponent), + multi: true + }] +}) +export class DeviceProfileConfigurationComponent implements ControlValueAccessor, OnInit { + + deviceProfileType = DeviceProfileType; + + deviceProfileConfigurationFormGroup: 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.deviceProfileConfigurationFormGroup = this.fb.group({ + configuration: [null, Validators.required] + }); + this.deviceProfileConfigurationFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileConfigurationFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileConfigurationFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileConfiguration | null): void { + this.type = value?.type; + const configuration = deepClone(value); + if (configuration) { + delete configuration.type; + } + this.deviceProfileConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); + } + + private updateModel() { + let configuration: DeviceProfileConfiguration = null; + if (this.deviceProfileConfigurationFormGroup.valid) { + configuration = this.deviceProfileConfigurationFormGroup.getRawValue().configuration; + configuration.type = this.type; + } + this.propagateChange(configuration); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts index bfa47f121f..04970bd833 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts @@ -35,8 +35,6 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit tenantProfileDataFormGroup: FormGroup; - modelValue: TenantProfileData | null; - private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -53,9 +51,6 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit constructor(private store: Store, private fb: FormBuilder) { - this.tenantProfileDataFormGroup = this.fb.group({ - tenantProfileData: [null, Validators.required] - }); } registerOnChange(fn: any): void { @@ -66,24 +61,33 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit } ngOnInit() { - this.tenantProfileDataFormGroup.get('tenantProfileData').valueChanges.subscribe( - tenantProfileData => { - this.updateView(this.tenantProfileDataFormGroup.valid ? tenantProfileData : null); - } - ); + this.tenantProfileDataFormGroup = this.fb.group({ + tenantProfileData: [null, Validators.required] + }); + this.tenantProfileDataFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.tenantProfileDataFormGroup.disable({emitEvent: false}); + } else { + this.tenantProfileDataFormGroup.enable({emitEvent: false}); + } } writeValue(value: TenantProfileData | null): void { - this.modelValue = value; this.tenantProfileDataFormGroup.get('tenantProfileData').patchValue(value, {emitEvent: false}); } - updateView(value: TenantProfileData | null) { - this.modelValue = value; - this.propagateChange(this.modelValue); + private updateModel() { + let tenantProfileData: TenantProfileData = null; + if (this.tenantProfileDataFormGroup.valid) { + tenantProfileData = this.tenantProfileDataFormGroup.getRawValue().profileData; + } + this.propagateChange(tenantProfileData); } + } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts new file mode 100644 index 0000000000..f0ffcd2c4d --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts @@ -0,0 +1,56 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; +import { Authority } from '@shared/models/authority.enum'; +import { DeviceProfilesTableConfigResolver } from './device-profiles-table-config.resolver'; + +const routes: Routes = [ + { + path: 'deviceProfiles', + data: { + breadcrumb: { + label: 'device-profile.device-profiles', + icon: 'mdi:alpha-d-box' + } + }, + children: [ + { + path: '', + component: EntitiesTableComponent, + data: { + auth: [Authority.TENANT_ADMIN], + title: 'device-profile.device-profiles' + }, + resolve: { + entitiesTableConfig: DeviceProfilesTableConfigResolver + } + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [ + DeviceProfilesTableConfigResolver + ] +}) +export class DeviceProfileRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html new file mode 100644 index 0000000000..40a9f55aa5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts new file mode 100644 index 0000000000..9cf18c498e --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts @@ -0,0 +1,38 @@ +/// +/// 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 } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; +import { DeviceProfile } from '@shared/models/device.models'; + +@Component({ + selector: 'tb-device-profile-tabs', + templateUrl: './device-profile-tabs.component.html', + styleUrls: [] +}) +export class DeviceProfileTabsComponent extends EntityTabsComponent { + + constructor(protected store: Store) { + super(store); + } + + ngOnInit() { + super.ngOnInit(); + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts new file mode 100644 index 0000000000..09207057ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts @@ -0,0 +1,35 @@ +/// +/// 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 { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '@shared/shared.module'; +import { HomeComponentsModule } from '@modules/home/components/home-components.module'; +import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; +import { DeviceProfileRoutingModule } from './device-profile-routing.module'; + +@NgModule({ + declarations: [ + DeviceProfileTabsComponent + ], + imports: [ + CommonModule, + SharedModule, + HomeComponentsModule, + DeviceProfileRoutingModule + ] +}) +export class DeviceProfileModule { } diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts new file mode 100644 index 0000000000..3de52418eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -0,0 +1,125 @@ +/// +/// 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 { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; +import { + checkBoxCell, + DateEntityTableColumn, + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { TranslateService } from '@ngx-translate/core'; +import { DatePipe } from '@angular/common'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { EntityAction } from '@home/models/entity/entity-component.models'; +import { DialogService } from '@core/services/dialog.service'; +import { DeviceProfile, deviceProfileTypeTranslationMap } from '@shared/models/device.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; +import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; + +@Injectable() +export class DeviceProfilesTableConfigResolver implements Resolve> { + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private deviceProfileService: DeviceProfileService, + private translate: TranslateService, + private datePipe: DatePipe, + private dialogService: DialogService) { + + this.config.entityType = EntityType.DEVICE_PROFILE; + this.config.entityComponent = DeviceProfileComponent; + this.config.entityTabsComponent = DeviceProfileTabsComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE_PROFILE); + this.config.entityResources = entityTypeResources.get(EntityType.DEVICE_PROFILE); + + this.config.columns.push( + new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), + new EntityTableColumn('name', 'device-profile.name', '20%'), + new EntityTableColumn('type', 'device-profile.type', '20%', (deviceProfile) => { + return this.translate.instant(deviceProfileTypeTranslationMap.get(deviceProfile.type)); + }), + new EntityTableColumn('description', 'device-profile.description', '60%'), + new EntityTableColumn('isDefault', 'device-profile.default', '60px', + entity => { + return checkBoxCell(entity.default); + }) + ); + + this.config.cellActionDescriptors.push( + { + name: this.translate.instant('device-profile.set-default'), + icon: 'flag', + isEnabled: (deviceProfile) => !deviceProfile.default, + onAction: ($event, entity) => this.setDefaultDeviceProfile($event, entity) + } + ); + + this.config.deleteEntityTitle = deviceProfile => this.translate.instant('device-profile.delete-device-profile-title', + { deviceProfileName: deviceProfile.name }); + this.config.deleteEntityContent = () => this.translate.instant('device-profile.delete-device-profile-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('device-profile.delete-device-profiles-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('device-profile.delete-device-profiles-text'); + + this.config.entitiesFetchFunction = pageLink => this.deviceProfileService.getDeviceProfiles(pageLink); + this.config.loadEntity = id => this.deviceProfileService.getDeviceProfile(id.id); + this.config.saveEntity = deviceProfile => this.deviceProfileService.saveDeviceProfile(deviceProfile); + this.config.deleteEntity = id => this.deviceProfileService.deleteDeviceProfile(id.id); + this.config.onEntityAction = action => this.onDeviceProfileAction(action); + this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; + } + + resolve(): EntityTableConfig { + this.config.tableTitle = this.translate.instant('device-profile.device-profiles'); + + return this.config; + } + + setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device-profile.set-default-device-profile-title', {deviceProfileName: deviceProfile.name}), + this.translate.instant('device-profile.set-default-device-profile-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceProfileService.setDefaultDeviceProfile(deviceProfile.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + onDeviceProfileAction(action: EntityAction): boolean { + switch (action.action) { + case 'setDefault': + this.setDefaultDeviceProfile(action.event, action.entity); + return true; + } + return false; + } + +} diff --git a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts index 5c8503eb2b..497c6231f3 100644 --- a/ui-ngx/src/app/modules/home/pages/home-pages.module.ts +++ b/ui-ngx/src/app/modules/home/pages/home-pages.module.ts @@ -32,6 +32,7 @@ import { DashboardModule } from '@modules/home/pages/dashboard/dashboard.module' import { TenantProfileModule } from './tenant-profile/tenant-profile.module'; import { MODULES_MAP } from '@shared/public-api'; import { modulesMap } from '../../common/modules-map'; +import { DeviceProfileModule } from './device-profile/device-profile.module'; @NgModule({ exports: [ @@ -40,6 +41,7 @@ import { modulesMap } from '../../common/modules-map'; ProfileModule, TenantProfileModule, TenantModule, + DeviceProfileModule, DeviceModule, AssetModule, EntityViewModule, diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index bb15f5b2c5..a0b90c7727 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -25,23 +25,38 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { EntityInfoData } from '@shared/models/entity.models'; export enum DeviceProfileType { - DEFAULT = 'DEFAULT', - LWM2M = 'LWM2M' + DEFAULT = 'DEFAULT' } +export const deviceProfileTypeTranslationMap = new Map( + [ + [DeviceProfileType.DEFAULT, 'device-profile.type-default'] + ] +); + export interface DefaultDeviceProfileConfiguration { [key: string]: any; } -export interface Lwm2mDeviceProfileConfiguration { - [key: string]: any; -} -export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration & Lwm2mDeviceProfileConfiguration; +export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration; export interface DeviceProfileConfiguration extends DeviceProfileConfigurations { type: DeviceProfileType; } +export function createDeviceProfileConfiguration(type: DeviceProfileType): DeviceProfileConfiguration { + let configuration: DeviceProfileConfiguration = null; + if (type) { + switch (type) { + case DeviceProfileType.DEFAULT: + const defaultConfiguration: DefaultDeviceProfileConfiguration = {}; + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; + break; + } + } + return configuration; +} + export interface DeviceProfileData { configuration: DeviceProfileConfiguration; } @@ -50,7 +65,7 @@ export interface DeviceProfile extends BaseData { tenantId?: TenantId; name: string; description?: string; - isDefault: boolean; + default: boolean; type: DeviceProfileType; defaultRuleChainId?: RuleChainId; profileData: DeviceProfileData; 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 68d7009f8e..7f6416ab7a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -754,10 +754,34 @@ "device-profile": "Device profile", "device-profiles": "Device profiles", "add": "Add device profile", + "edit": "Edit device profile", "device-profile-details": "Device profile details", "no-device-profiles-text": "No device profiles found", "search": "Search device profiles", - "selected-device-profiles": "{ count, plural, 1 {1 device profile} other {# device profiles} } selected" + "selected-device-profiles": "{ count, plural, 1 {1 device profile} other {# device profiles} } selected", + "no-device-profiles-matching": "No device profile matching '{{entity}}' were found.", + "device-profile-required": "Device profile is required", + "idCopiedMessage": "Device profile Id has been copied to clipboard", + "set-default": "Make device profile default", + "delete": "Delete device profile", + "copyId": "Copy device profile Id", + "name": "Name", + "name-required": "Name is required.", + "type": "Type", + "type-required": "Type is required.", + "type-default": "Default", + "description": "Description", + "default": "Default", + "profile-configuration": "Profile configuration", + "transport-configuration": "Transport configuration", + "delete-device-profile-title": "Are you sure you want to delete the device profile '{{deviceProfileName}}'?", + "delete-device-profile-text": "Be careful, after the confirmation the device profile and all related data will become unrecoverable.", + "delete-device-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 device profile} other {# device profiles} }?", + "delete-device-profiles-text": "Be careful, after the confirmation all selected device profiles will be removed and all related data will become unrecoverable.", + "set-default-device-profile-title": "Are you sure you want to make the device profile '{{deviceProfileName}}' default?", + "set-default-device-profile-text": "After the confirmation the device profile will be marked as default and will be used for new devices with no profile specified.", + "no-device-profiles-found": "No device profiles found.", + "create-new-device-profile": "Create a new one!" }, "dialog": { "close": "Close dialog" @@ -1700,7 +1724,7 @@ "delete-tenant-profile-text": "Be careful, after the confirmation the tenant profile and all related data will become unrecoverable.", "delete-tenant-profiles-title": "Are you sure you want to delete { count, plural, 1 {1 tenant profile} other {# tenant profiles} }?", "delete-tenant-profiles-text": "Be careful, after the confirmation all selected tenant profiles will be removed and all related data will become unrecoverable.", - "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' root?", + "set-default-tenant-profile-title": "Are you sure you want to make the tenant profile '{{tenantProfileName}}' default?", "set-default-tenant-profile-text": "After the confirmation the tenant profile will be marked as default and will be used for new tenants with no profile specified.", "no-tenant-profiles-found": "No tenant profiles found.", "create-new-tenant-profile": "Create a new one!"