Commit 3dc7fde4dbe67aa39b2a8aca1789e7459c2251d7

Authored by Igor Kulikov
1 parent c4e67215

UI: Add device profile wizard

... ... @@ -105,6 +105,7 @@ import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
105 105 import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
106 106 import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
107 107 import { FilterTextComponent } from './filter/filter-text.component';
  108 +import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component';
108 109
109 110 @NgModule({
110 111 declarations:
... ... @@ -192,7 +193,8 @@ import { FilterTextComponent } from './filter/filter-text.component';
192 193 DeviceProfileAlarmsComponent,
193 194 DeviceProfileDataComponent,
194 195 DeviceProfileComponent,
195   - DeviceProfileDialogComponent
  196 + DeviceProfileDialogComponent,
  197 + AddDeviceProfileDialogComponent
196 198 ],
197 199 imports: [
198 200 CommonModule,
... ... @@ -269,7 +271,8 @@ import { FilterTextComponent } from './filter/filter-text.component';
269 271 DeviceProfileAlarmsComponent,
270 272 DeviceProfileDataComponent,
271 273 DeviceProfileComponent,
272   - DeviceProfileDialogComponent
  274 + DeviceProfileDialogComponent,
  275 + AddDeviceProfileDialogComponent
273 276 ],
274 277 providers: [
275 278 WidgetComponentService,
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div style="min-width: 1000px;">
  19 + <mat-toolbar color="primary">
  20 + <h2 translate>device-profile.add</h2>
  21 + <span fxFlex></span>
  22 + <button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <mat-horizontal-stepper [linear]="true" #addDeviceProfileStepper (selectionChange)="selectedIndex = $event.selectedIndex">
  33 + <mat-step [stepControl]="deviceProfileDetailsFormGroup">
  34 + <form [formGroup]="deviceProfileDetailsFormGroup" style="padding-bottom: 16px;">
  35 + <ng-template matStepLabel>{{ 'device-profile.device-profile-details' | translate }}</ng-template>
  36 + <fieldset [disabled]="isLoading$ | async">
  37 + <mat-form-field class="mat-block">
  38 + <mat-label translate>device-profile.name</mat-label>
  39 + <input matInput formControlName="name" required/>
  40 + <mat-error *ngIf="deviceProfileDetailsFormGroup.get('name').hasError('required')">
  41 + {{ 'device-profile.name-required' | translate }}
  42 + </mat-error>
  43 + </mat-form-field>
  44 + <tb-entity-autocomplete
  45 + labelText="device-profile.default-rule-chain"
  46 + [entityType]="entityType.RULE_CHAIN"
  47 + formControlName="defaultRuleChainId">
  48 + </tb-entity-autocomplete>
  49 + <mat-form-field class="mat-block">
  50 + <mat-label translate>device-profile.type</mat-label>
  51 + <mat-select formControlName="type" required>
  52 + <mat-option *ngFor="let type of deviceProfileTypes" [value]="type">
  53 + {{deviceProfileTypeTranslations.get(type) | translate}}
  54 + </mat-option>
  55 + </mat-select>
  56 + <mat-error *ngIf="deviceProfileDetailsFormGroup.get('type').hasError('required')">
  57 + {{ 'device-profile.type-required' | translate }}
  58 + </mat-error>
  59 + </mat-form-field>
  60 + <mat-form-field class="mat-block">
  61 + <mat-label translate>device-profile.description</mat-label>
  62 + <textarea matInput formControlName="description" rows="2"></textarea>
  63 + </mat-form-field>
  64 + </fieldset>
  65 + </form>
  66 + </mat-step>
  67 + <mat-step [stepControl]="transportConfigFormGroup">
  68 + <form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
  69 + <ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
  70 + <mat-form-field class="mat-block">
  71 + <mat-label translate>device-profile.transport-type</mat-label>
  72 + <mat-select formControlName="transportType" required>
  73 + <mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
  74 + {{deviceTransportTypeTranslations.get(type) | translate}}
  75 + </mat-option>
  76 + </mat-select>
  77 + <mat-error *ngIf="transportConfigFormGroup.get('transportType').hasError('required')">
  78 + {{ 'device-profile.transport-type-required' | translate }}
  79 + </mat-error>
  80 + </mat-form-field>
  81 + <tb-device-profile-transport-configuration
  82 + formControlName="transportConfiguration"
  83 + required>
  84 + </tb-device-profile-transport-configuration>
  85 + </form>
  86 + </mat-step>
  87 + <mat-step [stepControl]="alarmRulesFormGroup">
  88 + <form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
  89 + <ng-template matStepLabel>{{'device-profile.alarm-rules' | translate:
  90 + {count: alarmRulesFormGroup.get('alarms').value ?
  91 + alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
  92 + <tb-device-profile-alarms
  93 + formControlName="alarms">
  94 + </tb-device-profile-alarms>
  95 + </form>
  96 + </mat-step>
  97 + </mat-horizontal-stepper>
  98 + </div>
  99 + <div mat-dialog-actions fxLayout="row wrap" fxLayoutAlign="space-between center">
  100 + <button mat-button *ngIf="selectedIndex > 0"
  101 + [disabled]="(isLoading$ | async)"
  102 + (click)="previousStep()">{{ 'action.back' | translate }}</button>
  103 + <span *ngIf="selectedIndex <= 0"></span>
  104 + <div fxLayout="row wrap" fxLayoutGap="20px">
  105 + <button mat-button
  106 + [disabled]="(isLoading$ | async)"
  107 + (click)="cancel()">{{ 'action.cancel' | translate }}</button>
  108 + <button mat-raised-button
  109 + [disabled]="(isLoading$ | async) || selectedForm().invalid"
  110 + color="primary"
  111 + (click)="nextStep()">{{ (selectedIndex === 2 ? 'action.add' : 'action.continue') | translate }}</button>
  112 + </div>
  113 + </div>
  114 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host {
  17 + .mat-dialog-content {
  18 + display: flex;
  19 + flex-direction: column;
  20 + overflow: hidden;
  21 +
  22 + .mat-stepper-horizontal {
  23 + display: flex;
  24 + flex-direction: column;
  25 + overflow: hidden;
  26 + }
  27 + }
  28 +}
  29 +
  30 +:host ::ng-deep {
  31 + .mat-dialog-content {
  32 + .mat-stepper-horizontal {
  33 + .mat-horizontal-content-container {
  34 + overflow: auto;
  35 + }
  36 + }
  37 + }
  38 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + ComponentFactoryResolver,
  21 + Inject,
  22 + Injector,
  23 + SkipSelf,
  24 + ViewChild
  25 +} from '@angular/core';
  26 +import { ErrorStateMatcher } from '@angular/material/core';
  27 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  28 +import { Store } from '@ngrx/store';
  29 +import { AppState } from '@core/core.state';
  30 +import { FormBuilder, FormGroup, Validators } from '@angular/forms';
  31 +import { DialogComponent } from '@shared/components/dialog.component';
  32 +import { Router } from '@angular/router';
  33 +import {
  34 + createDeviceProfileConfiguration,
  35 + createDeviceProfileTransportConfiguration,
  36 + DeviceProfile,
  37 + DeviceProfileType,
  38 + deviceProfileTypeTranslationMap,
  39 + DeviceTransportType,
  40 + deviceTransportTypeTranslationMap
  41 +} from '@shared/models/device.models';
  42 +import { DeviceProfileService } from '@core/http/device-profile.service';
  43 +import { EntityType } from '@shared/models/entity-type.models';
  44 +import { MatHorizontalStepper } from '@angular/material/stepper';
  45 +import { RuleChainId } from '@shared/models/id/rule-chain-id';
  46 +
  47 +export interface AddDeviceProfileDialogData {
  48 + deviceProfileName: string;
  49 +}
  50 +
  51 +@Component({
  52 + selector: 'tb-add-device-profile-dialog',
  53 + templateUrl: './add-device-profile-dialog.component.html',
  54 + providers: [],
  55 + styleUrls: ['./add-device-profile-dialog.component.scss']
  56 +})
  57 +export class AddDeviceProfileDialogComponent extends
  58 + DialogComponent<AddDeviceProfileDialogComponent, DeviceProfile> implements AfterViewInit {
  59 +
  60 + @ViewChild('addDeviceProfileStepper', {static: true}) addDeviceProfileStepper: MatHorizontalStepper;
  61 +
  62 + selectedIndex = 0;
  63 +
  64 + entityType = EntityType;
  65 +
  66 + deviceProfileTypes = Object.keys(DeviceProfileType);
  67 +
  68 + deviceProfileTypeTranslations = deviceProfileTypeTranslationMap;
  69 +
  70 + deviceTransportTypes = Object.keys(DeviceTransportType);
  71 +
  72 + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
  73 +
  74 + deviceProfileDetailsFormGroup: FormGroup;
  75 +
  76 + transportConfigFormGroup: FormGroup;
  77 +
  78 + alarmRulesFormGroup: FormGroup;
  79 +
  80 + constructor(protected store: Store<AppState>,
  81 + protected router: Router,
  82 + @Inject(MAT_DIALOG_DATA) public data: AddDeviceProfileDialogData,
  83 + public dialogRef: MatDialogRef<AddDeviceProfileDialogComponent, DeviceProfile>,
  84 + private componentFactoryResolver: ComponentFactoryResolver,
  85 + private injector: Injector,
  86 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  87 + private deviceProfileService: DeviceProfileService,
  88 + private fb: FormBuilder) {
  89 + super(store, router, dialogRef);
  90 + this.deviceProfileDetailsFormGroup = this.fb.group(
  91 + {
  92 + name: [data.deviceProfileName, [Validators.required]],
  93 + type: [DeviceProfileType.DEFAULT, [Validators.required]],
  94 + defaultRuleChainId: [null, []],
  95 + description: ['', []]
  96 + }
  97 + );
  98 + this.transportConfigFormGroup = this.fb.group(
  99 + {
  100 + transportType: [DeviceTransportType.DEFAULT, [Validators.required]],
  101 + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT),
  102 + [Validators.required]]
  103 + }
  104 + );
  105 + this.transportConfigFormGroup.get('transportType').valueChanges.subscribe(() => {
  106 + this.deviceProfileTransportTypeChanged();
  107 + });
  108 +
  109 + this.alarmRulesFormGroup = this.fb.group(
  110 + {
  111 + alarms: [null]
  112 + }
  113 + );
  114 + }
  115 +
  116 + private deviceProfileTransportTypeChanged() {
  117 + const deviceTransportType: DeviceTransportType = this.transportConfigFormGroup.get('transportType').value;
  118 + this.transportConfigFormGroup.patchValue(
  119 + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
  120 + }
  121 +
  122 + ngAfterViewInit(): void {
  123 + }
  124 +
  125 + cancel(): void {
  126 + this.dialogRef.close(null);
  127 + }
  128 +
  129 + previousStep() {
  130 + this.addDeviceProfileStepper.previous();
  131 + }
  132 +
  133 + nextStep() {
  134 + if (this.selectedIndex < 2) {
  135 + this.addDeviceProfileStepper.next();
  136 + } else {
  137 + this.add();
  138 + }
  139 + }
  140 +
  141 + selectedForm(): FormGroup {
  142 + switch (this.selectedIndex) {
  143 + case 0:
  144 + return this.deviceProfileDetailsFormGroup;
  145 + case 1:
  146 + return this.transportConfigFormGroup;
  147 + case 2:
  148 + return this.alarmRulesFormGroup;
  149 + }
  150 + }
  151 +
  152 + private add(): void {
  153 + const deviceProfile: DeviceProfile = {
  154 + name: this.deviceProfileDetailsFormGroup.get('name').value,
  155 + type: this.deviceProfileDetailsFormGroup.get('type').value,
  156 + transportType: this.transportConfigFormGroup.get('transportType').value,
  157 + description: this.deviceProfileDetailsFormGroup.get('description').value,
  158 + profileData: {
  159 + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
  160 + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
  161 + alarms: this.alarmRulesFormGroup.get('alarms').value
  162 + }
  163 + };
  164 + if (this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value) {
  165 + deviceProfile.defaultRuleChainId = new RuleChainId(this.deviceProfileDetailsFormGroup.get('defaultRuleChainId').value);
  166 + }
  167 + this.deviceProfileService.saveDeviceProfile(deviceProfile).subscribe(
  168 + (savedDeviceProfile) => {
  169 + this.dialogRef.close(savedDeviceProfile);
  170 + }
  171 + );
  172 + }
  173 +}
... ...
... ... @@ -32,12 +32,12 @@
32 32 </mat-slide-toggle>
33 33 </div>
34 34 <div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px">
35   - <span style="min-width: 250px;" [fxShow]="!enableDuration"></span>
36   - <div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration">
  35 + <span style="min-width: 250px;" *ngIf="!enableDuration"></span>
  36 + <div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" *ngIf="enableDuration">
37 37 <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
38 38 <mat-label></mat-label>
39 39 <input type="number"
40   - [required]="enableDuration"
  40 + required
41 41 step="1"
42 42 min="1" max="2147483647" matInput
43 43 placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
... ... @@ -55,7 +55,7 @@
55 55 <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
56 56 <mat-label></mat-label>
57 57 <mat-select formControlName="durationUnit"
58   - [required]="enableDuration"
  58 + required
59 59 placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
60 60 <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
61 61 {{ timeUnitTranslations.get(timeUnit) | translate }}
... ...
... ... @@ -46,7 +46,7 @@
46 46 <mat-icon>remove_circle_outline</mat-icon>
47 47 </button>
48 48 </div>
49   - <div *ngIf="disabled && !createAlarmRulesFormArray().controls.length">
  49 + <div *ngIf="!createAlarmRulesFormArray().controls.length">
50 50 <span translate fxLayoutAlign="center center"
51 51 class="tb-prompt">device-profile.no-create-alarm-rules</span>
52 52 </div>
... ...
... ... @@ -124,6 +124,9 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
124 124 this.updateModel();
125 125 });
126 126 this.updateUsedSeverities();
  127 + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
  128 + this.updateModel();
  129 + }
127 130 }
128 131
129 132 public removeCreateAlarmRule(index: number) {
... ...
... ... @@ -67,7 +67,7 @@
67 67 <mat-icon>remove_circle_outline</mat-icon>
68 68 </button>
69 69 </div>
70   - <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value">
  70 + <div *ngIf="!alarmFormGroup.get('clearRule').value">
71 71 <span translate fxLayoutAlign="center center"
72 72 class="tb-prompt">device-profile.no-clear-alarm-rule</span>
73 73 </div>
... ...
... ... @@ -63,7 +63,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
63 63
64 64 alarmFormGroup: FormGroup;
65 65
66   - private propagateChange = (v: any) => { };
  66 + private propagateChange = null;
  67 + private propagateChangePending = false;
67 68
68 69 constructor(private dialog: MatDialog,
69 70 private fb: FormBuilder) {
... ... @@ -71,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
71 72
72 73 registerOnChange(fn: any): void {
73 74 this.propagateChange = fn;
  75 + if (this.propagateChangePending) {
  76 + this.propagateChangePending = false;
  77 + setTimeout(() => {
  78 + this.propagateChange(this.modelValue);
  79 + }, 0);
  80 + }
74 81 }
75 82
76 83 registerOnTouched(fn: any): void {
... ... @@ -100,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
100 107 }
101 108
102 109 writeValue(value: DeviceProfileAlarm): void {
  110 + this.propagateChangePending = false;
103 111 this.modelValue = value;
104 112 if (!this.modelValue.alarmType) {
105 113 this.expanded = true;
106 114 }
107 115 this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
  116 + if (!this.disabled && !this.alarmFormGroup.valid) {
  117 + this.updateModel();
  118 + }
108 119 }
109 120
110 121 public addClearAlarmRule() {
... ... @@ -160,6 +171,10 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
160 171 private updateModel() {
161 172 const value = this.alarmFormGroup.value;
162 173 this.modelValue = {...this.modelValue, ...value};
163   - this.propagateChange(this.modelValue);
  174 + if (this.propagateChange) {
  175 + this.propagateChange(this.modelValue);
  176 + } else {
  177 + this.propagateChangePending = true;
  178 + }
164 179 }
165 180 }
... ...
... ... @@ -25,6 +25,10 @@
25 25 </tb-device-profile-alarm>
26 26 </div>
27 27 </div>
  28 + <div *ngIf="!alarmsFormArray().controls.length">
  29 + <span translate fxLayoutAlign="center center"
  30 + class="tb-prompt">device-profile.no-alarm-rules</span>
  31 + </div>
28 32 <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center"
29 33 style="padding-top: 16px;">
30 34 <button mat-raised-button color="primary"
... ...
... ... @@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
162 162 }
163 163
164 164 private updateModel() {
165   -// if (this.deviceProfileAlarmsFormGroup.valid) {
166   - const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;
167   - this.propagateChange(alarms);
168   - /* } else {
169   - this.propagateChange(null);
170   - } */
  165 + const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;
  166 + this.propagateChange(alarms);
171 167 }
172 168 }
... ...
... ... @@ -49,6 +49,7 @@ import {
49 49 import { DeviceProfileService } from '@core/http/device-profile.service';
50 50 import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component';
51 51 import { MatAutocomplete } from '@angular/material/autocomplete';
  52 +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component';
52 53
53 54 @Component({
54 55 selector: 'tb-device-profile-autocomplete',
... ... @@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
279 280 createDeviceProfile($event: Event, profileName: string) {
280 281 $event.preventDefault();
281 282 const deviceProfile: DeviceProfile = {
282   - id: null,
283   - name: profileName,
284   - type: DeviceProfileType.DEFAULT,
285   - transportType: DeviceTransportType.DEFAULT,
286   - profileData: {
287   - configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
288   - transportConfiguration: createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT)
289   - }
290   - };
  283 + name: profileName
  284 + } as DeviceProfile;
291 285 this.openDeviceProfileDialog(deviceProfile, true);
292 286 }
293 287
... ... @@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
301 295 }
302 296
303 297 openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) {
304   - this.dialog.open<DeviceProfileDialogComponent, DeviceProfileDialogData,
305   - DeviceProfile>(DeviceProfileDialogComponent, {
306   - disableClose: true,
307   - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
308   - data: {
309   - isAdd,
310   - deviceProfile
311   - }
312   - }).afterClosed().subscribe(
  298 + let deviceProfileObservable: Observable<DeviceProfile>;
  299 + if (!isAdd) {
  300 + deviceProfileObservable = this.dialog.open<DeviceProfileDialogComponent, DeviceProfileDialogData,
  301 + DeviceProfile>(DeviceProfileDialogComponent, {
  302 + disableClose: true,
  303 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  304 + data: {
  305 + isAdd: false,
  306 + deviceProfile
  307 + }
  308 + }).afterClosed();
  309 + } else {
  310 + deviceProfileObservable = this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
  311 + DeviceProfile>(AddDeviceProfileDialogComponent, {
  312 + disableClose: true,
  313 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  314 + data: {
  315 + deviceProfileName: deviceProfile.name
  316 + }
  317 + }).afterClosed();
  318 + }
  319 + deviceProfileObservable.subscribe(
313 320 (savedDeviceProfile) => {
314 321 if (!savedDeviceProfile) {
315 322 setTimeout(() => {
... ...
... ... @@ -81,7 +81,7 @@
81 81 required>
82 82 </tb-device-profile-data>
83 83 <mat-form-field class="mat-block">
84   - <mat-label translate>tenant-profile.description</mat-label>
  84 + <mat-label translate>device-profile.description</mat-label>
85 85 <textarea matInput formControlName="description" rows="2"></textarea>
86 86 </mat-form-field>
87 87 </fieldset>
... ...
... ... @@ -35,6 +35,12 @@ import {
35 35 import { DeviceProfileService } from '@core/http/device-profile.service';
36 36 import { DeviceProfileComponent } from '../../components/profile/device-profile.component';
37 37 import { DeviceProfileTabsComponent } from './device-profile-tabs.component';
  38 +import { Observable } from 'rxjs';
  39 +import { MatDialog } from '@angular/material/dialog';
  40 +import {
  41 + AddDeviceProfileDialogComponent,
  42 + AddDeviceProfileDialogData
  43 +} from '../../components/profile/add-device-profile-dialog.component';
38 44
39 45 @Injectable()
40 46 export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> {
... ... @@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
44 50 constructor(private deviceProfileService: DeviceProfileService,
45 51 private translate: TranslateService,
46 52 private datePipe: DatePipe,
47   - private dialogService: DialogService) {
  53 + private dialogService: DialogService,
  54 + private dialog: MatDialog) {
48 55
49 56 this.config.entityType = EntityType.DEVICE_PROFILE;
50 57 this.config.entityComponent = DeviceProfileComponent;
... ... @@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
92 99 this.config.onEntityAction = action => this.onDeviceProfileAction(action);
93 100 this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
94 101 this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default;
  102 + this.config.addEntity = () => this.addDeviceProfile();
95 103 }
96 104
97 105 resolve(): EntityTableConfig<DeviceProfile> {
... ... @@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
100 108 return this.config;
101 109 }
102 110
  111 + addDeviceProfile(): Observable<DeviceProfile> {
  112 + return this.dialog.open<AddDeviceProfileDialogComponent, AddDeviceProfileDialogData,
  113 + DeviceProfile>(AddDeviceProfileDialogComponent, {
  114 + disableClose: true,
  115 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  116 + data: {
  117 + deviceProfileName: null
  118 + }
  119 + }).afterClosed();
  120 + }
  121 +
103 122 setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) {
104 123 if ($event) {
105 124 $event.stopPropagation();
... ...
... ... @@ -811,6 +811,7 @@
811 811 "single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic filter level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
812 812 "multi-level-wildcards-hint": "<code>[#]</code> can replace the topic filter itself and must be the last symbol of the topic. Ex.: <b>#</b> or <b>v1/devices/me/#</b>.",
813 813 "alarm-rules": "Alarm rules ({{count}})",
  814 + "no-alarm-rules": "No alarm rules configured",
814 815 "add-alarm-rule": "Add alarm rule",
815 816 "edit-alarm-rule": "Edit alarm rule",
816 817 "alarm-type": "Alarm type",
... ...