Showing
15 changed files
with
409 additions
and
36 deletions
@@ -105,6 +105,7 @@ import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; | @@ -105,6 +105,7 @@ import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; | ||
105 | import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; | 105 | import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; |
106 | import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; | 106 | import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; |
107 | import { FilterTextComponent } from './filter/filter-text.component'; | 107 | import { FilterTextComponent } from './filter/filter-text.component'; |
108 | +import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; | ||
108 | 109 | ||
109 | @NgModule({ | 110 | @NgModule({ |
110 | declarations: | 111 | declarations: |
@@ -192,7 +193,8 @@ import { FilterTextComponent } from './filter/filter-text.component'; | @@ -192,7 +193,8 @@ import { FilterTextComponent } from './filter/filter-text.component'; | ||
192 | DeviceProfileAlarmsComponent, | 193 | DeviceProfileAlarmsComponent, |
193 | DeviceProfileDataComponent, | 194 | DeviceProfileDataComponent, |
194 | DeviceProfileComponent, | 195 | DeviceProfileComponent, |
195 | - DeviceProfileDialogComponent | 196 | + DeviceProfileDialogComponent, |
197 | + AddDeviceProfileDialogComponent | ||
196 | ], | 198 | ], |
197 | imports: [ | 199 | imports: [ |
198 | CommonModule, | 200 | CommonModule, |
@@ -269,7 +271,8 @@ import { FilterTextComponent } from './filter/filter-text.component'; | @@ -269,7 +271,8 @@ import { FilterTextComponent } from './filter/filter-text.component'; | ||
269 | DeviceProfileAlarmsComponent, | 271 | DeviceProfileAlarmsComponent, |
270 | DeviceProfileDataComponent, | 272 | DeviceProfileDataComponent, |
271 | DeviceProfileComponent, | 273 | DeviceProfileComponent, |
272 | - DeviceProfileDialogComponent | 274 | + DeviceProfileDialogComponent, |
275 | + AddDeviceProfileDialogComponent | ||
273 | ], | 276 | ], |
274 | providers: [ | 277 | providers: [ |
275 | WidgetComponentService, | 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,12 +32,12 @@ | ||
32 | </mat-slide-toggle> | 32 | </mat-slide-toggle> |
33 | </div> | 33 | </div> |
34 | <div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px"> | 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 | <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always"> | 37 | <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always"> |
38 | <mat-label></mat-label> | 38 | <mat-label></mat-label> |
39 | <input type="number" | 39 | <input type="number" |
40 | - [required]="enableDuration" | 40 | + required |
41 | step="1" | 41 | step="1" |
42 | min="1" max="2147483647" matInput | 42 | min="1" max="2147483647" matInput |
43 | placeholder="{{ 'device-profile.condition-duration-value' | translate }}" | 43 | placeholder="{{ 'device-profile.condition-duration-value' | translate }}" |
@@ -55,7 +55,7 @@ | @@ -55,7 +55,7 @@ | ||
55 | <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always"> | 55 | <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always"> |
56 | <mat-label></mat-label> | 56 | <mat-label></mat-label> |
57 | <mat-select formControlName="durationUnit" | 57 | <mat-select formControlName="durationUnit" |
58 | - [required]="enableDuration" | 58 | + required |
59 | placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> | 59 | placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> |
60 | <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> | 60 | <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> |
61 | {{ timeUnitTranslations.get(timeUnit) | translate }} | 61 | {{ timeUnitTranslations.get(timeUnit) | translate }} |
@@ -46,7 +46,7 @@ | @@ -46,7 +46,7 @@ | ||
46 | <mat-icon>remove_circle_outline</mat-icon> | 46 | <mat-icon>remove_circle_outline</mat-icon> |
47 | </button> | 47 | </button> |
48 | </div> | 48 | </div> |
49 | - <div *ngIf="disabled && !createAlarmRulesFormArray().controls.length"> | 49 | + <div *ngIf="!createAlarmRulesFormArray().controls.length"> |
50 | <span translate fxLayoutAlign="center center" | 50 | <span translate fxLayoutAlign="center center" |
51 | class="tb-prompt">device-profile.no-create-alarm-rules</span> | 51 | class="tb-prompt">device-profile.no-create-alarm-rules</span> |
52 | </div> | 52 | </div> |
@@ -124,6 +124,9 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, | @@ -124,6 +124,9 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, | ||
124 | this.updateModel(); | 124 | this.updateModel(); |
125 | }); | 125 | }); |
126 | this.updateUsedSeverities(); | 126 | this.updateUsedSeverities(); |
127 | + if (!this.disabled && !this.createAlarmRulesFormGroup.valid) { | ||
128 | + this.updateModel(); | ||
129 | + } | ||
127 | } | 130 | } |
128 | 131 | ||
129 | public removeCreateAlarmRule(index: number) { | 132 | public removeCreateAlarmRule(index: number) { |
@@ -67,7 +67,7 @@ | @@ -67,7 +67,7 @@ | ||
67 | <mat-icon>remove_circle_outline</mat-icon> | 67 | <mat-icon>remove_circle_outline</mat-icon> |
68 | </button> | 68 | </button> |
69 | </div> | 69 | </div> |
70 | - <div *ngIf="disabled && !alarmFormGroup.get('clearRule').value"> | 70 | + <div *ngIf="!alarmFormGroup.get('clearRule').value"> |
71 | <span translate fxLayoutAlign="center center" | 71 | <span translate fxLayoutAlign="center center" |
72 | class="tb-prompt">device-profile.no-clear-alarm-rule</span> | 72 | class="tb-prompt">device-profile.no-clear-alarm-rule</span> |
73 | </div> | 73 | </div> |
@@ -63,7 +63,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | @@ -63,7 +63,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | ||
63 | 63 | ||
64 | alarmFormGroup: FormGroup; | 64 | alarmFormGroup: FormGroup; |
65 | 65 | ||
66 | - private propagateChange = (v: any) => { }; | 66 | + private propagateChange = null; |
67 | + private propagateChangePending = false; | ||
67 | 68 | ||
68 | constructor(private dialog: MatDialog, | 69 | constructor(private dialog: MatDialog, |
69 | private fb: FormBuilder) { | 70 | private fb: FormBuilder) { |
@@ -71,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | @@ -71,6 +72,12 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | ||
71 | 72 | ||
72 | registerOnChange(fn: any): void { | 73 | registerOnChange(fn: any): void { |
73 | this.propagateChange = fn; | 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 | registerOnTouched(fn: any): void { | 83 | registerOnTouched(fn: any): void { |
@@ -100,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | @@ -100,11 +107,15 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | ||
100 | } | 107 | } |
101 | 108 | ||
102 | writeValue(value: DeviceProfileAlarm): void { | 109 | writeValue(value: DeviceProfileAlarm): void { |
110 | + this.propagateChangePending = false; | ||
103 | this.modelValue = value; | 111 | this.modelValue = value; |
104 | if (!this.modelValue.alarmType) { | 112 | if (!this.modelValue.alarmType) { |
105 | this.expanded = true; | 113 | this.expanded = true; |
106 | } | 114 | } |
107 | this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); | 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 | public addClearAlarmRule() { | 121 | public addClearAlarmRule() { |
@@ -160,6 +171,10 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | @@ -160,6 +171,10 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit | ||
160 | private updateModel() { | 171 | private updateModel() { |
161 | const value = this.alarmFormGroup.value; | 172 | const value = this.alarmFormGroup.value; |
162 | this.modelValue = {...this.modelValue, ...value}; | 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,6 +25,10 @@ | ||
25 | </tb-device-profile-alarm> | 25 | </tb-device-profile-alarm> |
26 | </div> | 26 | </div> |
27 | </div> | 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 | <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center" | 32 | <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="end center" |
29 | style="padding-top: 16px;"> | 33 | style="padding-top: 16px;"> |
30 | <button mat-raised-button color="primary" | 34 | <button mat-raised-button color="primary" |
@@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni | @@ -162,11 +162,7 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni | ||
162 | } | 162 | } |
163 | 163 | ||
164 | private updateModel() { | 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,6 +49,7 @@ import { | ||
49 | import { DeviceProfileService } from '@core/http/device-profile.service'; | 49 | import { DeviceProfileService } from '@core/http/device-profile.service'; |
50 | import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; | 50 | import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; |
51 | import { MatAutocomplete } from '@angular/material/autocomplete'; | 51 | import { MatAutocomplete } from '@angular/material/autocomplete'; |
52 | +import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './add-device-profile-dialog.component'; | ||
52 | 53 | ||
53 | @Component({ | 54 | @Component({ |
54 | selector: 'tb-device-profile-autocomplete', | 55 | selector: 'tb-device-profile-autocomplete', |
@@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, | @@ -279,15 +280,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, | ||
279 | createDeviceProfile($event: Event, profileName: string) { | 280 | createDeviceProfile($event: Event, profileName: string) { |
280 | $event.preventDefault(); | 281 | $event.preventDefault(); |
281 | const deviceProfile: DeviceProfile = { | 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 | this.openDeviceProfileDialog(deviceProfile, true); | 285 | this.openDeviceProfileDialog(deviceProfile, true); |
292 | } | 286 | } |
293 | 287 | ||
@@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, | @@ -301,15 +295,28 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, | ||
301 | } | 295 | } |
302 | 296 | ||
303 | openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { | 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 | (savedDeviceProfile) => { | 320 | (savedDeviceProfile) => { |
314 | if (!savedDeviceProfile) { | 321 | if (!savedDeviceProfile) { |
315 | setTimeout(() => { | 322 | setTimeout(() => { |
@@ -81,7 +81,7 @@ | @@ -81,7 +81,7 @@ | ||
81 | required> | 81 | required> |
82 | </tb-device-profile-data> | 82 | </tb-device-profile-data> |
83 | <mat-form-field class="mat-block"> | 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 | <textarea matInput formControlName="description" rows="2"></textarea> | 85 | <textarea matInput formControlName="description" rows="2"></textarea> |
86 | </mat-form-field> | 86 | </mat-form-field> |
87 | </fieldset> | 87 | </fieldset> |
@@ -35,6 +35,12 @@ import { | @@ -35,6 +35,12 @@ import { | ||
35 | import { DeviceProfileService } from '@core/http/device-profile.service'; | 35 | import { DeviceProfileService } from '@core/http/device-profile.service'; |
36 | import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; | 36 | import { DeviceProfileComponent } from '../../components/profile/device-profile.component'; |
37 | import { DeviceProfileTabsComponent } from './device-profile-tabs.component'; | 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 | @Injectable() | 45 | @Injectable() |
40 | export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> { | 46 | export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableConfig<DeviceProfile>> { |
@@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon | @@ -44,7 +50,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon | ||
44 | constructor(private deviceProfileService: DeviceProfileService, | 50 | constructor(private deviceProfileService: DeviceProfileService, |
45 | private translate: TranslateService, | 51 | private translate: TranslateService, |
46 | private datePipe: DatePipe, | 52 | private datePipe: DatePipe, |
47 | - private dialogService: DialogService) { | 53 | + private dialogService: DialogService, |
54 | + private dialog: MatDialog) { | ||
48 | 55 | ||
49 | this.config.entityType = EntityType.DEVICE_PROFILE; | 56 | this.config.entityType = EntityType.DEVICE_PROFILE; |
50 | this.config.entityComponent = DeviceProfileComponent; | 57 | this.config.entityComponent = DeviceProfileComponent; |
@@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon | @@ -92,6 +99,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon | ||
92 | this.config.onEntityAction = action => this.onDeviceProfileAction(action); | 99 | this.config.onEntityAction = action => this.onDeviceProfileAction(action); |
93 | this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; | 100 | this.config.deleteEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; |
94 | this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; | 101 | this.config.entitySelectionEnabled = (deviceProfile) => deviceProfile && !deviceProfile.default; |
102 | + this.config.addEntity = () => this.addDeviceProfile(); | ||
95 | } | 103 | } |
96 | 104 | ||
97 | resolve(): EntityTableConfig<DeviceProfile> { | 105 | resolve(): EntityTableConfig<DeviceProfile> { |
@@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon | @@ -100,6 +108,17 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon | ||
100 | return this.config; | 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 | setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { | 122 | setDefaultDeviceProfile($event: Event, deviceProfile: DeviceProfile) { |
104 | if ($event) { | 123 | if ($event) { |
105 | $event.stopPropagation(); | 124 | $event.stopPropagation(); |
@@ -811,6 +811,7 @@ | @@ -811,6 +811,7 @@ | ||
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>.", | 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 | "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>.", | 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 | "alarm-rules": "Alarm rules ({{count}})", | 813 | "alarm-rules": "Alarm rules ({{count}})", |
814 | + "no-alarm-rules": "No alarm rules configured", | ||
814 | "add-alarm-rule": "Add alarm rule", | 815 | "add-alarm-rule": "Add alarm rule", |
815 | "edit-alarm-rule": "Edit alarm rule", | 816 | "edit-alarm-rule": "Edit alarm rule", |
816 | "alarm-type": "Alarm type", | 817 | "alarm-type": "Alarm type", |