Browse Source

UI: Device profiles

pull/3477/head
Igor Kulikov 6 years ago
parent
commit
cc018f8876
  1. 66
      ui-ngx/src/app/core/http/device-profile.service.ts
  2. 13
      ui-ngx/src/app/core/services/menu.service.ts
  3. 16
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  4. 40
      ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html
  5. 92
      ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts
  6. 77
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  7. 128
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.ts
  8. 24
      ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html
  9. 97
      ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts
  10. 27
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html
  11. 103
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts
  12. 32
      ui-ngx/src/app/modules/home/components/profile/tenant-profile-data.component.ts
  13. 56
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts
  14. 43
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  15. 38
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  16. 35
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile.module.ts
  17. 125
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  18. 2
      ui-ngx/src/app/modules/home/pages/home-pages.module.ts
  19. 29
      ui-ngx/src/app/shared/models/device.models.ts
  20. 28
      ui-ngx/src/assets/locale/locale.constant-en_US.json

66
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<PageData<DeviceProfile>> {
return this.http.get<PageData<DeviceProfile>>(`/api/deviceProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
public getDeviceProfile(deviceProfileId: string, config?: RequestConfig): Observable<DeviceProfile> {
return this.http.get<DeviceProfile>(`/api/deviceProfile/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
}
public saveDeviceProfile(deviceProfile: DeviceProfile, config?: RequestConfig): Observable<DeviceProfile> {
return this.http.post<DeviceProfile>('/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<DeviceProfile> {
return this.http.post<DeviceProfile>(`/api/deviceProfile/${deviceProfileId}/default`, defaultHttpOptionsFromConfig(config));
}
public getDefaultDeviceProfileInfo(config?: RequestConfig): Observable<DeviceProfileInfo> {
return this.http.get<DeviceProfileInfo>('/api/deviceProfileInfo/default', defaultHttpOptionsFromConfig(config));
}
public getDeviceProfileInfo(deviceProfileId: string, config?: RequestConfig): Observable<DeviceProfileInfo> {
return this.http.get<DeviceProfileInfo>(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
}
public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable<PageData<DeviceProfileInfo>> {
return this.http.get<PageData<DeviceProfileInfo>>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
}
}

13
ui-ngx/src/app/core/services/menu.service.ts

@ -215,6 +215,13 @@ export class MenuService {
path: '/devices', path: '/devices',
icon: 'devices_other' icon: 'devices_other'
}, },
{
name: 'device-profile.device-profiles',
type: 'link',
path: '/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
},
{ {
name: 'entity-view.entity-views', name: 'entity-view.entity-views',
type: 'link', type: 'link',
@ -283,6 +290,12 @@ export class MenuService {
name: 'device.devices', name: 'device.devices',
icon: 'devices_other', icon: 'devices_other',
path: '/devices' path: '/devices'
},
{
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/deviceProfiles'
} }
] ]
}, },

16
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 { TenantProfileComponent } from './profile/tenant-profile.component';
import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component'; import { TenantProfileDialogComponent } from './profile/tenant-profile-dialog.component';
import { TenantProfileDataComponent } from './profile/tenant-profile-data.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({ @NgModule({
declarations: declarations:
@ -158,7 +162,11 @@ import { TenantProfileDataComponent } from './profile/tenant-profile-data.compon
TenantProfileAutocompleteComponent, TenantProfileAutocompleteComponent,
TenantProfileDataComponent, TenantProfileDataComponent,
TenantProfileComponent, TenantProfileComponent,
TenantProfileDialogComponent TenantProfileDialogComponent,
DefaultDeviceProfileConfigurationComponent,
DeviceProfileConfigurationComponent,
DeviceProfileDataComponent,
DeviceProfileComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -218,7 +226,11 @@ import { TenantProfileDataComponent } from './profile/tenant-profile-data.compon
TenantProfileAutocompleteComponent, TenantProfileAutocompleteComponent,
TenantProfileDataComponent, TenantProfileDataComponent,
TenantProfileComponent, TenantProfileComponent,
TenantProfileDialogComponent TenantProfileDialogComponent,
DefaultDeviceProfileConfigurationComponent,
DeviceProfileConfigurationComponent,
DeviceProfileDataComponent,
DeviceProfileComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

40
ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html

@ -0,0 +1,40 @@
<!--
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.
-->
<div [formGroup]="deviceProfileDataFormGroup">
<mat-accordion multi="true">
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>device-profile.profile-configuration</div>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-device-profile-configuration
formControlName="configuration"
required>
</tb-device-profile-configuration>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div translate>device-profile.transport-configuration</div>
</mat-panel-title>
</mat-expansion-panel-header>
TODO
</mat-expansion-panel>
</mat-accordion>
</div>

92
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<AppState>,
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);
}
}

77
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -0,0 +1,77 @@
<!--
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.
-->
<div class="tb-details-buttons" fxLayout.xs="column" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setDefault')"
[fxShow]="!isEdit && !entity?.default">
{{'device-profile.set-default' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'device-profile.delete' | translate }}
</button>
<div fxLayout="row" fxLayout.xs="column">
<button mat-raised-button
ngxClipboard
(cbOnSuccess)="onDeviceProfileIdCopied($event)"
[cbContent]="entity?.id?.id"
[fxShow]="!isEdit">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>device-profile.copyId</span>
</button>
</div>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.name</mat-label>
<input matInput formControlName="name" required/>
<mat-error *ngIf="entityForm.get('name').hasError('required')">
{{ 'device-profile.name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
{{deviceProfileTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-error *ngIf="entityForm.get('type').hasError('required')">
{{ 'device-profile.type-required' | translate }}
</mat-error>
</mat-form-field>
<tb-device-profile-data
formControlName="profileData"
required>
</tb-device-profile-data>
<tb-entity-autocomplete
[entityType]="entityType.RULE_CHAIN"
formControlName="defaultRuleChainId">
</tb-entity-autocomplete>
<mat-form-field class="mat-block">
<mat-label translate>tenant-profile.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</div>

128
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<DeviceProfile> {
@Input()
standalone = false;
entityType = EntityType;
deviceProfileTypes = Object.keys(DeviceProfileType);
deviceProfileTypeTranslations = deviceProfileTypeTranslationMap;
constructor(protected store: Store<AppState>,
protected translate: TranslateService,
@Optional() @Inject('entity') protected entityValue: DeviceProfile,
@Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>,
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'
}));
}
}

24
ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.html

@ -0,0 +1,24 @@
<!--
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.
-->
<form [formGroup]="defaultDeviceProfileConfigurationFormGroup" style="padding-bottom: 16px;">
<tb-json-object-edit
[required]="required"
label="{{ 'device-profile.type-default' | translate }}"
formControlName="configuration">
</tb-json-object-edit>
</form>

97
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<AppState>,
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);
}
}

27
ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html

@ -0,0 +1,27 @@
<!--
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.
-->
<div [formGroup]="deviceProfileConfigurationFormGroup">
<div [ngSwitch]="type">
<ng-template [ngSwitchCase]="deviceProfileType.DEFAULT">
<tb-default-device-profile-configuration
[required]="required"
formControlName="configuration">
</tb-default-device-profile-configuration>
</ng-template>
</div>
</div>

103
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<AppState>,
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);
}
}

32
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; tenantProfileDataFormGroup: FormGroup;
modelValue: TenantProfileData | null;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;
@ -53,9 +51,6 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private fb: FormBuilder) { private fb: FormBuilder) {
this.tenantProfileDataFormGroup = this.fb.group({
tenantProfileData: [null, Validators.required]
});
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -66,24 +61,33 @@ export class TenantProfileDataComponent implements ControlValueAccessor, OnInit
} }
ngOnInit() { ngOnInit() {
this.tenantProfileDataFormGroup.get('tenantProfileData').valueChanges.subscribe( this.tenantProfileDataFormGroup = this.fb.group({
tenantProfileData => { tenantProfileData: [null, Validators.required]
this.updateView(this.tenantProfileDataFormGroup.valid ? tenantProfileData : null); });
} this.tenantProfileDataFormGroup.valueChanges.subscribe(() => {
); this.updateModel();
});
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) {
this.tenantProfileDataFormGroup.disable({emitEvent: false});
} else {
this.tenantProfileDataFormGroup.enable({emitEvent: false});
}
} }
writeValue(value: TenantProfileData | null): void { writeValue(value: TenantProfileData | null): void {
this.modelValue = value;
this.tenantProfileDataFormGroup.get('tenantProfileData').patchValue(value, {emitEvent: false}); this.tenantProfileDataFormGroup.get('tenantProfileData').patchValue(value, {emitEvent: false});
} }
updateView(value: TenantProfileData | null) { private updateModel() {
this.modelValue = value; let tenantProfileData: TenantProfileData = null;
this.propagateChange(this.modelValue); if (this.tenantProfileDataFormGroup.valid) {
tenantProfileData = this.tenantProfileDataFormGroup.getRawValue().profileData;
}
this.propagateChange(tenantProfileData);
} }
} }

56
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 { }

43
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -0,0 +1,43 @@
<!--
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.
-->
<mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'tenant.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="nullUid"
[entityId]="entity.id"></tb-event-table>
</mat-tab>

38
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<DeviceProfile> {
constructor(protected store: Store<AppState>) {
super(store);
}
ngOnInit() {
super.ngOnInit();
}
}

35
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 { }

125
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<EntityTableConfig<DeviceProfile>> {
private readonly config: EntityTableConfig<DeviceProfile> = new EntityTableConfig<DeviceProfile>();
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<DeviceProfile>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<DeviceProfile>('name', 'device-profile.name', '20%'),
new EntityTableColumn<DeviceProfile>('type', 'device-profile.type', '20%', (deviceProfile) => {
return this.translate.instant(deviceProfileTypeTranslationMap.get(deviceProfile.type));
}),
new EntityTableColumn<DeviceProfile>('description', 'device-profile.description', '60%'),
new EntityTableColumn<DeviceProfile>('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<DeviceProfile> {
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<DeviceProfile>): boolean {
switch (action.action) {
case 'setDefault':
this.setDefaultDeviceProfile(action.event, action.entity);
return true;
}
return false;
}
}

2
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 { TenantProfileModule } from './tenant-profile/tenant-profile.module';
import { MODULES_MAP } from '@shared/public-api'; import { MODULES_MAP } from '@shared/public-api';
import { modulesMap } from '../../common/modules-map'; import { modulesMap } from '../../common/modules-map';
import { DeviceProfileModule } from './device-profile/device-profile.module';
@NgModule({ @NgModule({
exports: [ exports: [
@ -40,6 +41,7 @@ import { modulesMap } from '../../common/modules-map';
ProfileModule, ProfileModule,
TenantProfileModule, TenantProfileModule,
TenantModule, TenantModule,
DeviceProfileModule,
DeviceModule, DeviceModule,
AssetModule, AssetModule,
EntityViewModule, EntityViewModule,

29
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'; import { EntityInfoData } from '@shared/models/entity.models';
export enum DeviceProfileType { export enum DeviceProfileType {
DEFAULT = 'DEFAULT', DEFAULT = 'DEFAULT'
LWM2M = 'LWM2M'
} }
export const deviceProfileTypeTranslationMap = new Map<DeviceProfileType, string>(
[
[DeviceProfileType.DEFAULT, 'device-profile.type-default']
]
);
export interface DefaultDeviceProfileConfiguration { export interface DefaultDeviceProfileConfiguration {
[key: string]: any; [key: string]: any;
} }
export interface Lwm2mDeviceProfileConfiguration {
[key: string]: any;
}
export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration & Lwm2mDeviceProfileConfiguration; export type DeviceProfileConfigurations = DefaultDeviceProfileConfiguration;
export interface DeviceProfileConfiguration extends DeviceProfileConfigurations { export interface DeviceProfileConfiguration extends DeviceProfileConfigurations {
type: DeviceProfileType; 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 { export interface DeviceProfileData {
configuration: DeviceProfileConfiguration; configuration: DeviceProfileConfiguration;
} }
@ -50,7 +65,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId> {
tenantId?: TenantId; tenantId?: TenantId;
name: string; name: string;
description?: string; description?: string;
isDefault: boolean; default: boolean;
type: DeviceProfileType; type: DeviceProfileType;
defaultRuleChainId?: RuleChainId; defaultRuleChainId?: RuleChainId;
profileData: DeviceProfileData; profileData: DeviceProfileData;

28
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -754,10 +754,34 @@
"device-profile": "Device profile", "device-profile": "Device profile",
"device-profiles": "Device profiles", "device-profiles": "Device profiles",
"add": "Add device profile", "add": "Add device profile",
"edit": "Edit device profile",
"device-profile-details": "Device profile details", "device-profile-details": "Device profile details",
"no-device-profiles-text": "No device profiles found", "no-device-profiles-text": "No device profiles found",
"search": "Search device profiles", "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": { "dialog": {
"close": "Close 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-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-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.", "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.", "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.", "no-tenant-profiles-found": "No tenant profiles found.",
"create-new-tenant-profile": "Create a new one!" "create-new-tenant-profile": "Create a new one!"

Loading…
Cancel
Save