Showing
44 changed files
with
710 additions
and
336 deletions
common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKeyValueType.java
0 → 100644
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 | +package org.thingsboard.server.common.data.query; | |
17 | + | |
18 | +public enum EntityKeyValueType { | |
19 | + STRING, | |
20 | + NUMERIC, | |
21 | + BOOLEAN, | |
22 | + DATE_TIME | |
23 | +} | ... | ... |
... | ... | @@ -62,6 +62,7 @@ import { |
62 | 62 | entityInfoFields, |
63 | 63 | EntityKey, |
64 | 64 | EntityKeyType, |
65 | + EntityKeyValueType, | |
65 | 66 | FilterPredicateType, |
66 | 67 | singleEntityDataPageLink, |
67 | 68 | StringOperation |
... | ... | @@ -399,6 +400,7 @@ export class EntityService { |
399 | 400 | keyFilters: searchText && searchText.length ? [ |
400 | 401 | { |
401 | 402 | key: nameField, |
403 | + valueType: EntityKeyValueType.STRING, | |
402 | 404 | predicate: { |
403 | 405 | type: FilterPredicateType.STRING, |
404 | 406 | operation: StringOperation.STARTS_WITH, |
... | ... | @@ -593,10 +595,10 @@ export class EntityService { |
593 | 595 | return entityTypes; |
594 | 596 | } |
595 | 597 | |
596 | - private getEntityFieldKeys (entityType: EntityType, searchText: string): Array<string> { | |
598 | + private getEntityFieldKeys(entityType: EntityType, searchText: string): Array<string> { | |
597 | 599 | const entityFieldKeys: string[] = [entityFields.createdTime.keyName]; |
598 | 600 | const query = searchText.toLowerCase(); |
599 | - switch(entityType) { | |
601 | + switch (entityType) { | |
600 | 602 | case EntityType.USER: |
601 | 603 | entityFieldKeys.push(entityFields.name.keyName); |
602 | 604 | entityFieldKeys.push(entityFields.email.keyName); |
... | ... | @@ -863,7 +865,7 @@ export class EntityService { |
863 | 865 | const tasks: Observable<any>[] = []; |
864 | 866 | const result: Device | Asset = entity as (Device | Asset); |
865 | 867 | const additionalInfo = result.additionalInfo || {}; |
866 | - if(result.label !== entityData.label || | |
868 | + if (result.label !== entityData.label || | |
867 | 869 | result.type !== entityData.type || |
868 | 870 | additionalInfo.description !== entityData.description || |
869 | 871 | (result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) { | ... | ... |
... | ... | @@ -251,13 +251,14 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit |
251 | 251 | } |
252 | 252 | |
253 | 253 | onToggleEditMode(isEdit: boolean) { |
254 | - this.isEdit = isEdit; | |
255 | - if (!this.isEdit) { | |
254 | + if (!isEdit) { | |
256 | 255 | this.entityComponent.entity = this.entity; |
257 | 256 | if (this.entityTabsComponent) { |
258 | 257 | this.entityTabsComponent.entity = this.entity; |
259 | 258 | } |
259 | + this.isEdit = isEdit; | |
260 | 260 | } else { |
261 | + this.isEdit = isEdit; | |
261 | 262 | this.editingEntity = deepClone(this.entity); |
262 | 263 | this.entityComponent.entity = this.editingEntity; |
263 | 264 | if (this.entityTabsComponent) { | ... | ... |
... | ... | @@ -65,7 +65,6 @@ export abstract class EntityComponent<T extends BaseData<HasId>, |
65 | 65 | set entity(entity: T) { |
66 | 66 | this.entityValue = entity; |
67 | 67 | if (this.entityForm) { |
68 | - this.entityForm.reset(undefined, {emitEvent: false}); | |
69 | 68 | this.entityForm.markAsPristine(); |
70 | 69 | this.updateForm(entity); |
71 | 70 | } | ... | ... |
... | ... | @@ -37,6 +37,7 @@ |
37 | 37 | </mat-form-field> |
38 | 38 | <tb-filter-predicate-list |
39 | 39 | [valueType]="data.valueType" |
40 | + [displayUserParameters]="data.displayUserParameters" | |
40 | 41 | [operation]="complexFilterFormGroup.get('operation').value" |
41 | 42 | [key]="data.key" |
42 | 43 | formControlName="predicates"> |
... | ... | @@ -45,6 +46,7 @@ |
45 | 46 | </div> |
46 | 47 | <div mat-dialog-actions fxLayoutAlign="end center"> |
47 | 48 | <button mat-raised-button color="primary" |
49 | + *ngIf="!data.readonly" | |
48 | 50 | type="submit" |
49 | 51 | [disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty"> |
50 | 52 | {{ (isAdd ? 'action.add' : 'action.update') | translate }} |
... | ... | @@ -54,7 +56,7 @@ |
54 | 56 | [disabled]="(isLoading$ | async)" |
55 | 57 | (click)="cancel()" |
56 | 58 | cdkFocusInitial> |
57 | - {{ 'action.cancel' | translate }} | |
59 | + {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} | |
58 | 60 | </button> |
59 | 61 | </div> |
60 | 62 | </form> | ... | ... |
... | ... | @@ -32,9 +32,10 @@ import { |
32 | 32 | export interface ComplexFilterPredicateDialogData { |
33 | 33 | complexPredicate: ComplexFilterPredicateInfo; |
34 | 34 | key: string; |
35 | - disabled: boolean; | |
35 | + readonly: boolean; | |
36 | 36 | isAdd: boolean; |
37 | 37 | valueType: EntityKeyValueType; |
38 | + displayUserParameters: boolean; | |
38 | 39 | } |
39 | 40 | |
40 | 41 | @Component({ |
... | ... | @@ -73,6 +74,9 @@ export class ComplexFilterPredicateDialogComponent extends |
73 | 74 | predicates: [this.data.complexPredicate.predicates, [Validators.required]] |
74 | 75 | } |
75 | 76 | ); |
77 | + if (this.data.readonly) { | |
78 | + this.complexFilterFormGroup.disable({emitEvent: false}); | |
79 | + } | |
76 | 80 | } |
77 | 81 | |
78 | 82 | ngOnInit(): void { | ... | ... |
... | ... | @@ -19,11 +19,10 @@ |
19 | 19 | <mat-label translate>filter.complex-filter</mat-label> |
20 | 20 | <button mat-icon-button color="primary" |
21 | 21 | class="tb-mat-32" |
22 | - [fxShow]="!disabled" | |
23 | 22 | type="button" |
24 | 23 | (click)="openComplexFilterDialog()" |
25 | - matTooltip="{{ 'filter.edit-complex-filter' | translate }}" | |
24 | + matTooltip="{{ (disabled ? 'filter.complex-filter' : 'filter.edit-complex-filter') | translate }}" | |
26 | 25 | matTooltipPosition="above"> |
27 | - <mat-icon>edit</mat-icon> | |
26 | + <mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon> | |
28 | 27 | </button> |
29 | 28 | </div> | ... | ... |
... | ... | @@ -48,6 +48,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On |
48 | 48 | |
49 | 49 | @Input() key: string; |
50 | 50 | |
51 | + @Input() displayUserParameters = true; | |
52 | + | |
51 | 53 | private propagateChange = null; |
52 | 54 | |
53 | 55 | private complexFilterPredicate: ComplexFilterPredicateInfo; |
... | ... | @@ -79,11 +81,12 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On |
79 | 81 | disableClose: true, |
80 | 82 | panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
81 | 83 | data: { |
82 | - complexPredicate: deepClone(this.complexFilterPredicate), | |
83 | - disabled: this.disabled, | |
84 | + complexPredicate: this.disabled ? this.complexFilterPredicate : deepClone(this.complexFilterPredicate), | |
85 | + readonly: this.disabled, | |
84 | 86 | valueType: this.valueType, |
85 | 87 | isAdd: false, |
86 | - key: this.key | |
88 | + key: this.key, | |
89 | + displayUserParameters: this.displayUserParameters | |
87 | 90 | } |
88 | 91 | }).afterClosed().subscribe( |
89 | 92 | (result) => { | ... | ... |
... | ... | @@ -33,7 +33,8 @@ |
33 | 33 | </div> |
34 | 34 | <label fxFlex="60" translate class="tb-title no-padding">filter.value</label> |
35 | 35 | </div> |
36 | - <label translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label> | |
36 | + <label *ngIf="displayUserParameters" | |
37 | + translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label> | |
37 | 38 | <span [fxShow]="!disabled" style="min-width: 40px;"> </span> |
38 | 39 | </div> |
39 | 40 | </div> |
... | ... | @@ -50,6 +51,7 @@ |
50 | 51 | <tb-filter-predicate |
51 | 52 | fxFlex |
52 | 53 | [valueType]="valueType" |
54 | + [displayUserParameters]="displayUserParameters" | |
53 | 55 | [key]="key" |
54 | 56 | [formControl]="predicateControl"> |
55 | 57 | </tb-filter-predicate> | ... | ... |
... | ... | @@ -62,6 +62,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni |
62 | 62 | |
63 | 63 | @Input() operation: ComplexOperation = ComplexOperation.AND; |
64 | 64 | |
65 | + @Input() displayUserParameters = true; | |
66 | + | |
65 | 67 | filterListFormGroup: FormGroup; |
66 | 68 | |
67 | 69 | valueTypeEnum = EntityKeyValueType; |
... | ... | @@ -150,10 +152,11 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni |
150 | 152 | panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
151 | 153 | data: { |
152 | 154 | complexPredicate: predicate.keyFilterPredicate as ComplexFilterPredicateInfo, |
153 | - disabled: this.disabled, | |
155 | + readonly: this.disabled, | |
154 | 156 | valueType: this.valueType, |
155 | 157 | key: this.key, |
156 | - isAdd: true | |
158 | + isAdd: true, | |
159 | + displayUserParameters: this.displayUserParameters | |
157 | 160 | } |
158 | 161 | }).afterClosed().pipe( |
159 | 162 | map((result) => { | ... | ... |
... | ... | @@ -35,11 +35,12 @@ |
35 | 35 | <tb-complex-filter-predicate |
36 | 36 | [key]="key" |
37 | 37 | [valueType]="valueType" |
38 | + [displayUserParameters]="displayUserParameters" | |
38 | 39 | formControlName="predicate"> |
39 | 40 | </tb-complex-filter-predicate> |
40 | 41 | </ng-template> |
41 | 42 | </div> |
42 | - <tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX" | |
43 | + <tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX && displayUserParameters" | |
43 | 44 | style="width: 60px;" |
44 | 45 | fxLayout="row" fxLayoutAlign="center" |
45 | 46 | [valueType]="valueType" | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | --> |
18 | 18 | <form [formGroup]="filterUserInfoFormGroup" (ngSubmit)="save()" style="width: 500px;"> |
19 | 19 | <mat-toolbar color="primary"> |
20 | - <h2 translate>filter.edit-filter-user-params</h2> | |
20 | + <h2>{{(data.readonly ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate}}</h2> | |
21 | 21 | <span fxFlex></span> |
22 | 22 | <button mat-icon-button |
23 | 23 | (click)="cancel()" |
... | ... | @@ -47,6 +47,7 @@ |
47 | 47 | </div> |
48 | 48 | <div mat-dialog-actions fxLayoutAlign="end center"> |
49 | 49 | <button mat-raised-button color="primary" |
50 | + *ngIf="!data.readonly" | |
50 | 51 | type="submit" |
51 | 52 | [disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty"> |
52 | 53 | {{ 'action.update' | translate }} |
... | ... | @@ -56,7 +57,7 @@ |
56 | 57 | [disabled]="(isLoading$ | async)" |
57 | 58 | (click)="cancel()" |
58 | 59 | cdkFocusInitial> |
59 | - {{ 'action.cancel' | translate }} | |
60 | + {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} | |
60 | 61 | </button> |
61 | 62 | </div> |
62 | 63 | </form> | ... | ... |
... | ... | @@ -23,7 +23,7 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida |
23 | 23 | import { Router } from '@angular/router'; |
24 | 24 | import { DialogComponent } from '@app/shared/components/dialog.component'; |
25 | 25 | import { |
26 | - BooleanOperation, | |
26 | + BooleanOperation, createDefaultFilterPredicateUserInfo, | |
27 | 27 | EntityKeyValueType, generateUserFilterValueLabel, |
28 | 28 | KeyFilterPredicateUserInfo, NumericOperation, |
29 | 29 | StringOperation |
... | ... | @@ -35,6 +35,7 @@ export interface FilterUserInfoDialogData { |
35 | 35 | valueType: EntityKeyValueType; |
36 | 36 | operation: StringOperation | BooleanOperation | NumericOperation; |
37 | 37 | keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo; |
38 | + readonly: boolean; | |
38 | 39 | } |
39 | 40 | |
40 | 41 | @Component({ |
... | ... | @@ -60,18 +61,24 @@ export class FilterUserInfoDialogComponent extends |
60 | 61 | private translate: TranslateService) { |
61 | 62 | super(store, router, dialogRef); |
62 | 63 | |
64 | + const userInfo: KeyFilterPredicateUserInfo = this.data.keyFilterPredicateUserInfo || createDefaultFilterPredicateUserInfo(); | |
65 | + | |
63 | 66 | this.filterUserInfoFormGroup = this.fb.group( |
64 | 67 | { |
65 | - editable: [this.data.keyFilterPredicateUserInfo.editable], | |
66 | - label: [this.data.keyFilterPredicateUserInfo.label], | |
67 | - autogeneratedLabel: [this.data.keyFilterPredicateUserInfo.autogeneratedLabel], | |
68 | - order: [this.data.keyFilterPredicateUserInfo.order] | |
68 | + editable: [userInfo.editable], | |
69 | + label: [userInfo.label], | |
70 | + autogeneratedLabel: [userInfo.autogeneratedLabel], | |
71 | + order: [userInfo.order] | |
69 | 72 | } |
70 | 73 | ); |
71 | 74 | this.onAutogeneratedLabelChange(); |
72 | - this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => { | |
73 | - this.onAutogeneratedLabelChange(); | |
74 | - }); | |
75 | + if (!this.data.readonly) { | |
76 | + this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => { | |
77 | + this.onAutogeneratedLabelChange(); | |
78 | + }); | |
79 | + } else { | |
80 | + this.filterUserInfoFormGroup.disable({emitEvent: false}); | |
81 | + } | |
75 | 82 | } |
76 | 83 | |
77 | 84 | private onAutogeneratedLabelChange() { | ... | ... |
... | ... | @@ -17,10 +17,9 @@ |
17 | 17 | --> |
18 | 18 | <button mat-icon-button color="primary" |
19 | 19 | class="tb-mat-32" |
20 | - [fxShow]="!disabled" | |
21 | 20 | type="button" |
22 | 21 | (click)="openFilterUserInfoDialog()" |
23 | - matTooltip="{{ 'filter.edit-filter-user-params' | translate }}" | |
22 | + matTooltip="{{ (disabled ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate }}" | |
24 | 23 | matTooltipPosition="above"> |
25 | 24 | <mat-icon>settings</mat-icon> |
26 | 25 | </button> | ... | ... |
... | ... | @@ -76,7 +76,7 @@ export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { |
76 | 76 | this.keyFilterPredicateUserInfo = keyFilterPredicateUserInfo; |
77 | 77 | } |
78 | 78 | |
79 | - private openFilterUserInfoDialog() { | |
79 | + public openFilterUserInfoDialog() { | |
80 | 80 | this.dialog.open<FilterUserInfoDialogComponent, FilterUserInfoDialogData, |
81 | 81 | KeyFilterPredicateUserInfo>(FilterUserInfoDialogComponent, { |
82 | 82 | disableClose: true, |
... | ... | @@ -85,7 +85,8 @@ export class FilterUserInfoComponent implements ControlValueAccessor, OnInit { |
85 | 85 | keyFilterPredicateUserInfo: deepClone(this.keyFilterPredicateUserInfo), |
86 | 86 | valueType: this.valueType, |
87 | 87 | key: this.key, |
88 | - operation: this.operation | |
88 | + operation: this.operation, | |
89 | + readonly: this.disabled | |
89 | 90 | } |
90 | 91 | }).afterClosed().subscribe( |
91 | 92 | (result) => { | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | --> |
18 | 18 | <form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 900px;"> |
19 | 19 | <mat-toolbar color="primary"> |
20 | - <h2>{{(data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate}}</h2> | |
20 | + <h2>{{(data.isAdd ? 'filter.add-key-filter' : (data.readonly ? 'filter.key-filter' : 'filter.edit-key-filter')) | translate}}</h2> | |
21 | 21 | <span fxFlex></span> |
22 | 22 | <button mat-icon-button |
23 | 23 | (click)="cancel()" |
... | ... | @@ -70,6 +70,7 @@ |
70 | 70 | </mat-form-field> |
71 | 71 | </section> |
72 | 72 | <tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value" |
73 | + [displayUserParameters]="data.displayUserParameters" | |
73 | 74 | [valueType]="keyFilterFormGroup.get('valueType').value" |
74 | 75 | [key]="keyFilterFormGroup.get('key.key').value" |
75 | 76 | formControlName="predicates"> |
... | ... | @@ -79,6 +80,7 @@ |
79 | 80 | <div mat-dialog-actions fxLayoutAlign="end center"> |
80 | 81 | <button mat-raised-button color="primary" |
81 | 82 | type="submit" |
83 | + *ngIf="!data.readonly" | |
82 | 84 | [disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty"> |
83 | 85 | {{ (data.isAdd ? 'action.add' : 'action.update') | translate }} |
84 | 86 | </button> |
... | ... | @@ -87,7 +89,7 @@ |
87 | 89 | [disabled]="(isLoading$ | async)" |
88 | 90 | (click)="cancel()" |
89 | 91 | cdkFocusInitial> |
90 | - {{ 'action.cancel' | translate }} | |
92 | + {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} | |
91 | 93 | </button> |
92 | 94 | </div> |
93 | 95 | </form> | ... | ... |
... | ... | @@ -39,6 +39,9 @@ import { filter, map, startWith } from 'rxjs/operators'; |
39 | 39 | export interface KeyFilterDialogData { |
40 | 40 | keyFilter: KeyFilterInfo; |
41 | 41 | isAdd: boolean; |
42 | + displayUserParameters: boolean; | |
43 | + readonly: boolean; | |
44 | + telemetryKeysOnly: boolean; | |
42 | 45 | } |
43 | 46 | |
44 | 47 | @Component({ |
... | ... | @@ -53,7 +56,10 @@ export class KeyFilterDialogComponent extends |
53 | 56 | |
54 | 57 | keyFilterFormGroup: FormGroup; |
55 | 58 | |
56 | - entityKeyTypes = [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; | |
59 | + entityKeyTypes = | |
60 | + this.data.telemetryKeysOnly ? | |
61 | + [EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES] : | |
62 | + [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES]; | |
57 | 63 | |
58 | 64 | entityKeyTypeTranslations = entityKeyTypeTranslationMap; |
59 | 65 | |
... | ... | @@ -95,32 +101,37 @@ export class KeyFilterDialogComponent extends |
95 | 101 | predicates: [this.data.keyFilter.predicates, [Validators.required]] |
96 | 102 | } |
97 | 103 | ); |
98 | - this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { | |
99 | - const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; | |
100 | - const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; | |
101 | - if (prevValue && prevValue !== valueType && predicates && predicates.length) { | |
102 | - this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), | |
103 | - this.translate.instant('filter.key-value-type-change-message')).subscribe( | |
104 | - (result) => { | |
105 | - if (result) { | |
106 | - this.keyFilterFormGroup.get('predicates').setValue([]); | |
107 | - } else { | |
108 | - this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false}); | |
104 | + | |
105 | + if (!this.data.readonly) { | |
106 | + this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => { | |
107 | + const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; | |
108 | + const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value; | |
109 | + if (prevValue && prevValue !== valueType && predicates && predicates.length) { | |
110 | + this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'), | |
111 | + this.translate.instant('filter.key-value-type-change-message')).subscribe( | |
112 | + (result) => { | |
113 | + if (result) { | |
114 | + this.keyFilterFormGroup.get('predicates').setValue([]); | |
115 | + } else { | |
116 | + this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false}); | |
117 | + } | |
109 | 118 | } |
110 | - } | |
111 | - ); | |
112 | - } | |
113 | - }); | |
114 | - | |
115 | - this.keyFilterFormGroup.get('key.key').valueChanges.pipe( | |
116 | - filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) | |
117 | - ).subscribe((keyName: string) => { | |
118 | - const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; | |
119 | - const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; | |
120 | - if (prevValueType !== newValueType) { | |
121 | - this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); | |
122 | - } | |
123 | - }); | |
119 | + ); | |
120 | + } | |
121 | + }); | |
122 | + | |
123 | + this.keyFilterFormGroup.get('key.key').valueChanges.pipe( | |
124 | + filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName)) | |
125 | + ).subscribe((keyName: string) => { | |
126 | + const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType; | |
127 | + const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING; | |
128 | + if (prevValueType !== newValueType) { | |
129 | + this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false}); | |
130 | + } | |
131 | + }); | |
132 | + } else { | |
133 | + this.keyFilterFormGroup.disable({emitEvent: false}); | |
134 | + } | |
124 | 135 | |
125 | 136 | this.entityFields = entityFields; |
126 | 137 | this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort(); | ... | ... |
... | ... | @@ -46,9 +46,9 @@ |
46 | 46 | <button mat-icon-button color="primary" |
47 | 47 | type="button" |
48 | 48 | (click)="editKeyFilter($index)" |
49 | - matTooltip="{{ 'filter.edit-key-filter' | translate }}" | |
49 | + matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}" | |
50 | 50 | matTooltipPosition="above"> |
51 | - <mat-icon>edit</mat-icon> | |
51 | + <mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon> | |
52 | 52 | </button> |
53 | 53 | <button mat-icon-button color="primary" |
54 | 54 | [fxShow]="!disabled" | ... | ... |
... | ... | @@ -46,6 +46,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { |
46 | 46 | |
47 | 47 | @Input() disabled: boolean; |
48 | 48 | |
49 | + @Input() displayUserParameters = true; | |
50 | + | |
51 | + @Input() telemetryKeysOnly = false; | |
52 | + | |
49 | 53 | keyFilterListFormGroup: FormGroup; |
50 | 54 | |
51 | 55 | entityKeyTypeTranslations = entityKeyTypeTranslationMap; |
... | ... | @@ -147,8 +151,11 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit { |
147 | 151 | disableClose: true, |
148 | 152 | panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
149 | 153 | data: { |
150 | - keyFilter: keyFilter ? deepClone(keyFilter): null, | |
151 | - isAdd | |
154 | + keyFilter: keyFilter ? (this.disabled ? keyFilter : deepClone(keyFilter)) : null, | |
155 | + isAdd, | |
156 | + readonly: this.disabled, | |
157 | + displayUserParameters: this.displayUserParameters, | |
158 | + telemetryKeysOnly: this.telemetryKeysOnly | |
152 | 159 | } |
153 | 160 | }).afterClosed(); |
154 | 161 | } | ... | ... |
... | ... | @@ -100,10 +100,10 @@ import { MqttDeviceProfileTransportConfigurationComponent } from './profile/devi |
100 | 100 | import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component'; |
101 | 101 | import { DeviceProfileAlarmsComponent } from './profile/alarm/device-profile-alarms.component'; |
102 | 102 | import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alarm.component'; |
103 | -import { DeviceProfileAlarmDialogComponent } from './profile/alarm/device-profile-alarm-dialog.component'; | |
104 | 103 | import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component'; |
105 | 104 | import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component'; |
106 | 105 | import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component'; |
106 | +import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component'; | |
107 | 107 | |
108 | 108 | @NgModule({ |
109 | 109 | declarations: |
... | ... | @@ -184,9 +184,9 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio |
184 | 184 | DeviceProfileTransportConfigurationComponent, |
185 | 185 | CreateAlarmRulesComponent, |
186 | 186 | AlarmRuleComponent, |
187 | + AlarmRuleKeyFiltersDialogComponent, | |
187 | 188 | AlarmRuleConditionComponent, |
188 | 189 | DeviceProfileAlarmComponent, |
189 | - DeviceProfileAlarmDialogComponent, | |
190 | 190 | DeviceProfileAlarmsComponent, |
191 | 191 | DeviceProfileDataComponent, |
192 | 192 | DeviceProfileComponent, |
... | ... | @@ -260,9 +260,9 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio |
260 | 260 | DeviceProfileTransportConfigurationComponent, |
261 | 261 | CreateAlarmRulesComponent, |
262 | 262 | AlarmRuleComponent, |
263 | + AlarmRuleKeyFiltersDialogComponent, | |
263 | 264 | AlarmRuleConditionComponent, |
264 | 265 | DeviceProfileAlarmComponent, |
265 | - DeviceProfileAlarmDialogComponent, | |
266 | 266 | DeviceProfileAlarmsComponent, |
267 | 267 | DeviceProfileDataComponent, |
268 | 268 | DeviceProfileComponent, | ... | ... |
... | ... | @@ -15,5 +15,16 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<div fxLayout="row" [formGroup]="alarmRuleConditionFormGroup"> | |
19 | -</div> | |
18 | +<mat-form-field class="mat-block" (click)="openFilterDialog($event)" floatLabel="always" hideRequiredMarker> | |
19 | + <mat-label></mat-label> | |
20 | + <input readonly | |
21 | + required matInput [formControl]="alarmRuleConditionControl" | |
22 | + placeholder="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> | |
23 | + <a matSuffix mat-icon-button color="primary" | |
24 | + type="button" | |
25 | + (click)="openFilterDialog($event)" | |
26 | + matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}" | |
27 | + matTooltipPosition="above"> | |
28 | + <mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon> | |
29 | + </a> | |
30 | +</mat-form-field> | ... | ... |
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 | + a.mat-icon-button { | |
18 | + &:hover, &:focus { | |
19 | + border-bottom: none; | |
20 | + } | |
21 | + } | |
22 | +} | ... | ... |
... | ... | @@ -19,19 +19,22 @@ import { |
19 | 19 | ControlValueAccessor, |
20 | 20 | FormBuilder, |
21 | 21 | FormControl, |
22 | - FormGroup, | |
23 | 22 | NG_VALIDATORS, |
24 | 23 | NG_VALUE_ACCESSOR, |
25 | - Validator, | |
26 | - Validators | |
24 | + Validator | |
27 | 25 | } from '@angular/forms'; |
28 | -import { AlarmCondition } from '@shared/models/device.models'; | |
29 | 26 | import { MatDialog } from '@angular/material/dialog'; |
27 | +import { KeyFilter } from '@shared/models/query/query.models'; | |
28 | +import { deepClone } from '@core/utils'; | |
29 | +import { | |
30 | + AlarmRuleKeyFiltersDialogComponent, | |
31 | + AlarmRuleKeyFiltersDialogData | |
32 | +} from './alarm-rule-key-filters-dialog.component'; | |
30 | 33 | |
31 | 34 | @Component({ |
32 | 35 | selector: 'tb-alarm-rule-condition', |
33 | 36 | templateUrl: './alarm-rule-condition.component.html', |
34 | - styleUrls: [], | |
37 | + styleUrls: ['./alarm-rule-condition.component.scss'], | |
35 | 38 | providers: [ |
36 | 39 | { |
37 | 40 | provide: NG_VALUE_ACCESSOR, |
... | ... | @@ -50,9 +53,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit |
50 | 53 | @Input() |
51 | 54 | disabled: boolean; |
52 | 55 | |
53 | - private modelValue: AlarmCondition; | |
56 | + alarmRuleConditionControl: FormControl; | |
54 | 57 | |
55 | - alarmRuleConditionFormGroup: FormGroup; | |
58 | + private modelValue: Array<KeyFilter>; | |
56 | 59 | |
57 | 60 | private propagateChange = (v: any) => { }; |
58 | 61 | |
... | ... | @@ -68,45 +71,56 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit |
68 | 71 | } |
69 | 72 | |
70 | 73 | ngOnInit() { |
71 | - this.alarmRuleConditionFormGroup = this.fb.group({ | |
72 | - condition: [null, Validators.required], | |
73 | - durationUnit: [null], | |
74 | - durationValue: [null] | |
75 | - }); | |
76 | - this.alarmRuleConditionFormGroup.valueChanges.subscribe(() => { | |
77 | - this.updateModel(); | |
78 | - }); | |
74 | + this.alarmRuleConditionControl = this.fb.control(null); | |
79 | 75 | } |
80 | 76 | |
81 | 77 | setDisabledState(isDisabled: boolean): void { |
82 | 78 | this.disabled = isDisabled; |
83 | - if (this.disabled) { | |
84 | - this.alarmRuleConditionFormGroup.disable({emitEvent: false}); | |
85 | - } else { | |
86 | - this.alarmRuleConditionFormGroup.enable({emitEvent: false}); | |
87 | - } | |
88 | 79 | } |
89 | 80 | |
90 | - writeValue(value: AlarmCondition): void { | |
81 | + writeValue(value: Array<KeyFilter>): void { | |
91 | 82 | this.modelValue = value; |
92 | - this.alarmRuleConditionFormGroup.reset(this.modelValue, {emitEvent: false}); | |
83 | + this.updateConditionInfo(); | |
93 | 84 | } |
94 | 85 | |
95 | 86 | public validate(c: FormControl) { |
96 | - return (this.alarmRuleConditionFormGroup.valid) ? null : { | |
87 | + return (this.modelValue && this.modelValue.length) ? null : { | |
97 | 88 | alarmRuleCondition: { |
98 | 89 | valid: false, |
99 | 90 | }, |
100 | 91 | }; |
101 | 92 | } |
102 | 93 | |
103 | - private updateModel() { | |
104 | - if (this.alarmRuleConditionFormGroup.valid) { | |
105 | - const value = this.alarmRuleConditionFormGroup.value; | |
106 | - this.modelValue = {...this.modelValue, ...value}; | |
107 | - this.propagateChange(this.modelValue); | |
94 | + public openFilterDialog($event: Event) { | |
95 | + if ($event) { | |
96 | + $event.stopPropagation(); | |
97 | + } | |
98 | + this.dialog.open<AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData, | |
99 | + Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, { | |
100 | + disableClose: true, | |
101 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
102 | + data: { | |
103 | + readonly: this.disabled, | |
104 | + keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue) | |
105 | + } | |
106 | + }).afterClosed().subscribe((result) => { | |
107 | + if (result) { | |
108 | + this.modelValue = result; | |
109 | + this.updateModel(); | |
110 | + } | |
111 | + }); | |
112 | + } | |
113 | + | |
114 | + private updateConditionInfo() { | |
115 | + if (this.modelValue && this.modelValue.length) { | |
116 | + this.alarmRuleConditionControl.patchValue('Condition set'); | |
108 | 117 | } else { |
109 | - this.propagateChange(null); | |
118 | + this.alarmRuleConditionControl.patchValue(null); | |
110 | 119 | } |
111 | 120 | } |
121 | + | |
122 | + private updateModel() { | |
123 | + this.updateConditionInfo(); | |
124 | + this.propagateChange(this.modelValue); | |
125 | + } | |
112 | 126 | } | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.html
renamed from
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.html
... | ... | @@ -15,11 +15,11 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<form [formGroup]="alarmFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> | |
19 | - <mat-toolbar fxLayout="row" color="primary"> | |
20 | - <h2>{{ (isReadOnly ? 'device-profile.alarm-rule-details' : (isAdd ? 'device-profile.add-alarm-rule' : 'device-profile.edit-alarm-rule')) | translate }}</h2> | |
18 | +<form [formGroup]="keyFiltersFormGroup" (ngSubmit)="save()" style="width: 700px;"> | |
19 | + <mat-toolbar color="primary"> | |
20 | + <h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2> | |
21 | 21 | <span fxFlex></span> |
22 | - <button mat-button mat-icon-button | |
22 | + <button mat-icon-button | |
23 | 23 | (click)="cancel()" |
24 | 24 | type="button"> |
25 | 25 | <mat-icon class="material-icons">close</mat-icon> |
... | ... | @@ -27,39 +27,29 @@ |
27 | 27 | </mat-toolbar> |
28 | 28 | <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
29 | 29 | </mat-progress-bar> |
30 | - <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
31 | 30 | <div mat-dialog-content> |
32 | - <fieldset [disabled]="(isLoading$ | async) || isReadOnly" fxLayout="column"> | |
33 | - <mat-form-field fxFlex class="mat-block"> | |
34 | - <mat-label translate>device-profile.alarm-type</mat-label> | |
35 | - <input required matInput formControlName="alarmType"> | |
36 | - <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')"> | |
37 | - {{ 'device-profile.alarm-type-required' | translate }} | |
38 | - </mat-error> | |
39 | - <mat-hint innerHTML="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"></mat-hint> | |
40 | - </mat-form-field> | |
41 | - <fieldset> | |
42 | - <legend translate>device-profile.create-alarm-rules</legend> | |
43 | - </fieldset> | |
44 | - <fieldset> | |
45 | - <legend translate>device-profile.clear-alarm-rule</legend> | |
46 | - </fieldset> | |
31 | + <fieldset [disabled]="isLoading$ | async"> | |
32 | + <div fxFlex fxLayout="column"> | |
33 | + <tb-key-filter-list | |
34 | + [displayUserParameters]="false" | |
35 | + [telemetryKeysOnly]="true" | |
36 | + formControlName="keyFilters"> | |
37 | + </tb-key-filter-list> | |
38 | + </div> | |
47 | 39 | </fieldset> |
48 | 40 | </div> |
49 | - <div mat-dialog-actions fxLayout="row"> | |
50 | - <span fxFlex></span> | |
51 | - <button *ngIf="!isReadOnly" mat-button mat-raised-button color="primary" | |
41 | + <div mat-dialog-actions fxLayoutAlign="end center"> | |
42 | + <button mat-raised-button color="primary" | |
43 | + *ngIf="!readonly" | |
52 | 44 | type="submit" |
53 | - [disabled]="(isLoading$ | async) || alarmFormGroup.invalid | |
54 | - || !alarmFormGroup.dirty"> | |
55 | - {{ (isAdd ? 'action.add' : 'action.save') | translate }} | |
45 | + [disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty"> | |
46 | + {{ 'action.save' | translate }} | |
56 | 47 | </button> |
57 | 48 | <button mat-button color="primary" |
58 | - style="margin-right: 20px;" | |
59 | 49 | type="button" |
60 | 50 | [disabled]="(isLoading$ | async)" |
61 | 51 | (click)="cancel()" cdkFocusInitial> |
62 | - {{ (isReadOnly ? 'action.close' : 'action.cancel') | translate }} | |
52 | + {{ (readonly ? 'action.close' : 'action.cancel') | translate }} | |
63 | 53 | </button> |
64 | 54 | </div> |
65 | 55 | </form> | ... | ... |
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-key-filters-dialog.component.ts
renamed from
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.ts
... | ... | @@ -14,70 +14,60 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { | |
18 | - Component, | |
19 | - Inject, | |
20 | - OnInit, | |
21 | - SkipSelf | |
22 | -} from '@angular/core'; | |
17 | +import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; | |
23 | 18 | import { ErrorStateMatcher } from '@angular/material/core'; |
24 | 19 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
25 | 20 | import { Store } from '@ngrx/store'; |
26 | 21 | import { AppState } from '@core/core.state'; |
27 | 22 | import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
28 | -import { DialogComponent } from '@shared/components/dialog.component'; | |
29 | 23 | import { Router } from '@angular/router'; |
30 | -import { DeviceProfileAlarm } from '@shared/models/device.models'; | |
24 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | |
25 | +import { UtilsService } from '@core/services/utils.service'; | |
26 | +import { TranslateService } from '@ngx-translate/core'; | |
27 | +import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models'; | |
31 | 28 | |
32 | -export interface DeviceProfileAlarmDialogData { | |
33 | - alarm: DeviceProfileAlarm; | |
34 | - isAdd: boolean; | |
35 | - isReadOnly: boolean; | |
29 | +export interface AlarmRuleKeyFiltersDialogData { | |
30 | + readonly: boolean; | |
31 | + keyFilters: Array<KeyFilter>; | |
36 | 32 | } |
37 | 33 | |
38 | 34 | @Component({ |
39 | - selector: 'tb-device-profile-alarm-dialog', | |
40 | - templateUrl: './device-profile-alarm-dialog.component.html', | |
41 | - providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileAlarmDialogComponent}], | |
35 | + selector: 'tb-alarm-rule-key-filters-dialog', | |
36 | + templateUrl: './alarm-rule-key-filters-dialog.component.html', | |
37 | + providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}], | |
42 | 38 | styleUrls: [] |
43 | 39 | }) |
44 | -export class DeviceProfileAlarmDialogComponent extends | |
45 | - DialogComponent<DeviceProfileAlarmDialogComponent, DeviceProfileAlarm> implements OnInit, ErrorStateMatcher { | |
40 | +export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>> | |
41 | + implements OnInit, ErrorStateMatcher { | |
46 | 42 | |
47 | - alarmFormGroup: FormGroup; | |
43 | + readonly = this.data.readonly; | |
44 | + keyFilters = this.data.keyFilters; | |
48 | 45 | |
49 | - isReadOnly = this.data.isReadOnly; | |
50 | - alarm = this.data.alarm; | |
51 | - isAdd = this.data.isAdd; | |
46 | + keyFiltersFormGroup: FormGroup; | |
52 | 47 | |
53 | 48 | submitted = false; |
54 | 49 | |
55 | 50 | constructor(protected store: Store<AppState>, |
56 | 51 | protected router: Router, |
57 | - @Inject(MAT_DIALOG_DATA) public data: DeviceProfileAlarmDialogData, | |
58 | - public dialogRef: MatDialogRef<DeviceProfileAlarmDialogComponent, DeviceProfileAlarm>, | |
52 | + @Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData, | |
59 | 53 | @SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
60 | - public fb: FormBuilder) { | |
54 | + public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>, | |
55 | + private fb: FormBuilder, | |
56 | + private utils: UtilsService, | |
57 | + public translate: TranslateService) { | |
61 | 58 | super(store, router, dialogRef); |
62 | - this.isAdd = this.data.isAdd; | |
63 | - this.alarm = this.data.alarm; | |
64 | - } | |
65 | 59 | |
66 | - ngOnInit(): void { | |
67 | - this.alarmFormGroup = this.fb.group({ | |
68 | - id: [null, Validators.required], | |
69 | - alarmType: [null, Validators.required], | |
70 | - createRules: [null], | |
71 | - clearRule: [null], | |
72 | - propagate: [null], | |
73 | - propagateRelationTypes: [null] | |
60 | + this.keyFiltersFormGroup = this.fb.group({ | |
61 | + keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required] | |
74 | 62 | }); |
75 | - this.alarmFormGroup.reset(this.alarm, {emitEvent: false}); | |
76 | - if (this.isReadOnly) { | |
77 | - this.alarmFormGroup.disable({emitEvent: false}); | |
63 | + if (this.readonly) { | |
64 | + this.keyFiltersFormGroup.disable({emitEvent: false}); | |
78 | 65 | } |
79 | 66 | } |
80 | 67 | |
68 | + ngOnInit(): void { | |
69 | + } | |
70 | + | |
81 | 71 | isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { |
82 | 72 | const originalErrorState = this.errorStateMatcher.isErrorState(control, form); |
83 | 73 | const customErrorState = !!(control && control.invalid && this.submitted); |
... | ... | @@ -90,10 +80,7 @@ export class DeviceProfileAlarmDialogComponent extends |
90 | 80 | |
91 | 81 | save(): void { |
92 | 82 | this.submitted = true; |
93 | - if (this.alarmFormGroup.valid) { | |
94 | - this.alarm = {...this.alarm, ...this.alarmFormGroup.value}; | |
95 | - this.dialogRef.close(this.alarm); | |
96 | - } | |
83 | + this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value); | |
84 | + this.dialogRef.close(this.keyFilters); | |
97 | 85 | } |
98 | - | |
99 | 86 | } | ... | ... |
... | ... | @@ -15,8 +15,71 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<div fxLayout="row" [formGroup]="alarmRuleFormGroup"> | |
19 | - <tb-alarm-rule-condition | |
20 | - formControlName="condition"> | |
21 | - </tb-alarm-rule-condition> | |
18 | +<div fxLayout="column" [formGroup]="alarmRuleFormGroup"> | |
19 | + <div formGroupName="condition" fxLayout="row" fxLayoutAlign="start" fxLayoutGap="8px" fxFlex> | |
20 | + <div fxLayout="column" fxFlex> | |
21 | + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> | |
22 | + <label class="tb-small" translate>device-profile.alarm-rule-condition</label> | |
23 | + </div> | |
24 | + <tb-alarm-rule-condition fxFlex | |
25 | + formControlName="condition"> | |
26 | + </tb-alarm-rule-condition> | |
27 | + </div> | |
28 | + <div fxLayout="column" style="min-width: 250px;"> | |
29 | + <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> | |
30 | + <label fxFlex class="tb-small" translate>device-profile.condition-duration</label> | |
31 | + <mat-slide-toggle [disabled]="disabled" | |
32 | + [ngModelOptions]="{standalone: true}" | |
33 | + (ngModelChange)="enableDurationChanged($event)" | |
34 | + [ngModel]="enableDuration"> | |
35 | + </mat-slide-toggle> | |
36 | + </div> | |
37 | + <div fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration"> | |
38 | + <mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always"> | |
39 | + <mat-label></mat-label> | |
40 | + <input type="number" | |
41 | + [required]="enableDuration" | |
42 | + step="1" | |
43 | + min="1" max="2147483647" matInput | |
44 | + placeholder="{{ 'device-profile.condition-duration-value' | translate }}" | |
45 | + formControlName="durationValue"> | |
46 | + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')"> | |
47 | + {{ 'device-profile.condition-duration-value-required' | translate }} | |
48 | + </mat-error> | |
49 | + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')"> | |
50 | + {{ 'device-profile.condition-duration-value-range' | translate }} | |
51 | + </mat-error> | |
52 | + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')"> | |
53 | + {{ 'device-profile.condition-duration-value-range' | translate }} | |
54 | + </mat-error> | |
55 | + </mat-form-field> | |
56 | + <mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always"> | |
57 | + <mat-label></mat-label> | |
58 | + <mat-select formControlName="durationUnit" | |
59 | + [required]="enableDuration" | |
60 | + placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> | |
61 | + <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> | |
62 | + {{ timeUnitTranslations.get(timeUnit) | translate }} | |
63 | + </mat-option> | |
64 | + </mat-select> | |
65 | + <mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')"> | |
66 | + {{ 'device-profile.condition-duration-time-unit-required' | translate }} | |
67 | + </mat-error> | |
68 | + </mat-form-field> | |
69 | + </div> | |
70 | + </div> | |
71 | + </div> | |
72 | + <mat-expansion-panel class="advanced-settings" [expanded]="false"> | |
73 | + <mat-expansion-panel-header> | |
74 | + <mat-panel-title> | |
75 | + <div fxFlex fxLayout="row" fxLayoutAlign="end center"> | |
76 | + <div class="tb-small" translate>device-profile.advanced-settings</div> | |
77 | + </div> | |
78 | + </mat-panel-title> | |
79 | + </mat-expansion-panel-header> | |
80 | + <mat-form-field class="mat-block"> | |
81 | + <mat-label translate>device-profile.alarm-details</mat-label> | |
82 | + <textarea matInput formControlName="alarmDetails" rows="5"></textarea> | |
83 | + </mat-form-field> | |
84 | + </mat-expansion-panel> | |
22 | 85 | </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-expansion-panel.advanced-settings { | |
18 | + box-shadow: none; | |
19 | + border: none; | |
20 | + padding: 0; | |
21 | + } | |
22 | +} | |
23 | + | |
24 | +:host ::ng-deep { | |
25 | + .mat-expansion-panel.advanced-settings { | |
26 | + .mat-expansion-panel-body { | |
27 | + padding: 0; | |
28 | + } | |
29 | + } | |
30 | + .mat-form-field.duration-value-field { | |
31 | + .mat-form-field-infix { | |
32 | + width: 120px; | |
33 | + } | |
34 | + } | |
35 | + .mat-form-field.duration-unit-field { | |
36 | + .mat-form-field-infix { | |
37 | + width: 120px; | |
38 | + } | |
39 | + } | |
40 | +} | |
41 | + | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, forwardRef, Input, OnInit } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, forwardRef, Input, NgZone, OnInit } from '@angular/core'; | |
18 | 18 | import { |
19 | 19 | ControlValueAccessor, |
20 | 20 | FormBuilder, |
... | ... | @@ -27,11 +27,13 @@ import { |
27 | 27 | } from '@angular/forms'; |
28 | 28 | import { AlarmRule } from '@shared/models/device.models'; |
29 | 29 | import { MatDialog } from '@angular/material/dialog'; |
30 | +import { TimeUnit, timeUnitTranslationMap } from '../../../../../shared/models/time/time.models'; | |
31 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
30 | 32 | |
31 | 33 | @Component({ |
32 | 34 | selector: 'tb-alarm-rule', |
33 | 35 | templateUrl: './alarm-rule.component.html', |
34 | - styleUrls: [], | |
36 | + styleUrls: ['./alarm-rule.component.scss'], | |
35 | 37 | providers: [ |
36 | 38 | { |
37 | 39 | provide: NG_VALUE_ACCESSOR, |
... | ... | @@ -47,9 +49,23 @@ import { MatDialog } from '@angular/material/dialog'; |
47 | 49 | }) |
48 | 50 | export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { |
49 | 51 | |
52 | + timeUnits = Object.keys(TimeUnit); | |
53 | + timeUnitTranslations = timeUnitTranslationMap; | |
54 | + | |
50 | 55 | @Input() |
51 | 56 | disabled: boolean; |
52 | 57 | |
58 | + private requiredValue: boolean; | |
59 | + get required(): boolean { | |
60 | + return this.requiredValue; | |
61 | + } | |
62 | + @Input() | |
63 | + set required(value: boolean) { | |
64 | + this.requiredValue = coerceBooleanProperty(value); | |
65 | + } | |
66 | + | |
67 | + enableDuration = false; | |
68 | + | |
53 | 69 | private modelValue: AlarmRule; |
54 | 70 | |
55 | 71 | alarmRuleFormGroup: FormGroup; |
... | ... | @@ -69,7 +85,11 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat |
69 | 85 | |
70 | 86 | ngOnInit() { |
71 | 87 | this.alarmRuleFormGroup = this.fb.group({ |
72 | - condition: [null, Validators.required], | |
88 | + condition: this.fb.group({ | |
89 | + condition: [null, Validators.required], | |
90 | + durationUnit: [null], | |
91 | + durationValue: [null] | |
92 | + }, Validators.required), | |
73 | 93 | alarmDetails: [null] |
74 | 94 | }); |
75 | 95 | this.alarmRuleFormGroup.valueChanges.subscribe(() => { |
... | ... | @@ -88,24 +108,51 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat |
88 | 108 | |
89 | 109 | writeValue(value: AlarmRule): void { |
90 | 110 | this.modelValue = value; |
91 | - this.alarmRuleFormGroup.reset(this.modelValue, {emitEvent: false}); | |
111 | + this.enableDuration = value && !!value.condition.durationValue; | |
112 | + this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); | |
113 | + this.updateValidators(); | |
92 | 114 | } |
93 | 115 | |
94 | 116 | public validate(c: FormControl) { |
95 | - return (this.alarmRuleFormGroup.valid) ? null : { | |
117 | + return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : { | |
96 | 118 | alarmRule: { |
97 | 119 | valid: false, |
98 | 120 | }, |
99 | 121 | }; |
100 | 122 | } |
101 | 123 | |
124 | + public enableDurationChanged(enableDuration) { | |
125 | + this.enableDuration = enableDuration; | |
126 | + this.updateValidators(true, true); | |
127 | + } | |
128 | + | |
129 | + private updateValidators(resetDuration = false, emitEvent = false) { | |
130 | + if (this.enableDuration) { | |
131 | + this.alarmRuleFormGroup.get('condition').get('durationValue') | |
132 | + .setValidators([Validators.required, Validators.min(1), Validators.max(2147483647)]); | |
133 | + this.alarmRuleFormGroup.get('condition').get('durationUnit') | |
134 | + .setValidators([Validators.required]); | |
135 | + } else { | |
136 | + this.alarmRuleFormGroup.get('condition').get('durationValue') | |
137 | + .setValidators([]); | |
138 | + this.alarmRuleFormGroup.get('condition').get('durationUnit') | |
139 | + .setValidators([]); | |
140 | + if (resetDuration) { | |
141 | + this.alarmRuleFormGroup.get('condition').patchValue({ | |
142 | + durationValue: null, | |
143 | + durationUnit: null | |
144 | + }); | |
145 | + } | |
146 | + } | |
147 | + this.alarmRuleFormGroup.get('condition').get('durationValue').updateValueAndValidity({emitEvent}); | |
148 | + this.alarmRuleFormGroup.get('condition').get('durationUnit').updateValueAndValidity({emitEvent}); | |
149 | + } | |
150 | + | |
102 | 151 | private updateModel() { |
103 | - if (this.alarmRuleFormGroup.valid) { | |
104 | - const value = this.alarmRuleFormGroup.value; | |
152 | + const value = this.alarmRuleFormGroup.value; | |
153 | + if (this.modelValue) { | |
105 | 154 | this.modelValue = {...this.modelValue, ...value}; |
106 | 155 | this.propagateChange(this.modelValue); |
107 | - } else { | |
108 | - this.propagateChange(null); | |
109 | 156 | } |
110 | 157 | } |
111 | 158 | } | ... | ... |
... | ... | @@ -17,38 +17,42 @@ |
17 | 17 | --> |
18 | 18 | <div fxFlex fxLayout="column"> |
19 | 19 | <div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index; |
20 | - last as isLast;" fxLayout="row" style="padding-left: 20px;" [formGroup]="createAlarmRuleControl"> | |
21 | - <mat-form-field class="mat-block" floatLabel="always" hideRequiredMarker> | |
22 | - <mat-label translate></mat-label> | |
23 | - <mat-select formControlName="severity" | |
24 | - required | |
25 | - placeholder="{{ 'device-profile.select-alarm-severity' | translate }}"> | |
26 | - <mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> | |
27 | - {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} | |
28 | - </mat-option> | |
29 | - </mat-select> | |
30 | - <mat-error *ngIf="createAlarmRuleControl.get('severity').hasError('required')"> | |
31 | - {{ 'device-profile.alarm-severity-required' | translate }} | |
32 | - </mat-error> | |
33 | - </mat-form-field> | |
34 | - <tb-alarm-rule formControlName="alarmRule"> | |
35 | - </tb-alarm-rule> | |
20 | + last as isLast;" fxLayout="row" fxLayoutAlign="start center" | |
21 | + fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl"> | |
22 | + <div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start"> | |
23 | + <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker> | |
24 | + <mat-label translate>alarm.severity</mat-label> | |
25 | + <mat-select formControlName="severity" | |
26 | + required | |
27 | + placeholder="{{ 'device-profile.select-alarm-severity' | translate }}"> | |
28 | + <mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> | |
29 | + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} | |
30 | + </mat-option> | |
31 | + </mat-select> | |
32 | + <mat-error *ngIf="createAlarmRuleControl.get('severity').hasError('required')"> | |
33 | + {{ 'device-profile.alarm-severity-required' | translate }} | |
34 | + </mat-error> | |
35 | + </mat-form-field> | |
36 | + <tb-alarm-rule formControlName="alarmRule" required fxFlex> | |
37 | + </tb-alarm-rule> | |
38 | + </div> | |
36 | 39 | <button *ngIf="!disabled && createAlarmRulesFormArray().controls.length > 1" |
37 | 40 | mat-icon-button color="primary" style="min-width: 40px;" |
38 | 41 | type="button" |
39 | 42 | (click)="removeCreateAlarmRule($index)" |
40 | 43 | matTooltip="{{ 'action.remove' | translate }}" |
41 | 44 | matTooltipPosition="above"> |
42 | - <mat-icon>close</mat-icon> | |
45 | + <mat-icon>remove_circle_outline</mat-icon> | |
43 | 46 | </button> |
44 | 47 | </div> |
45 | - <div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center"> | |
46 | - <button mat-icon-button color="primary" | |
48 | + <div fxLayout="row" *ngIf="!disabled"> | |
49 | + <button mat-stroked-button color="primary" | |
47 | 50 | type="button" |
48 | 51 | (click)="addCreateAlarmRule()" |
49 | 52 | matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}" |
50 | 53 | matTooltipPosition="above"> |
51 | - <mat-icon>add</mat-icon> | |
54 | + <mat-icon>add_circle_outline</mat-icon> | |
55 | + {{ 'device-profile.add-create-alarm-rule' | translate }} | |
52 | 56 | </button> |
53 | 57 | </div> |
54 | 58 | </div> | ... | ... |
... | ... | @@ -13,6 +13,23 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | + | |
16 | 17 | :host { |
18 | + .create-alarm-rule { | |
19 | + border: 1px groove rgba(0, 0, 0, .25); | |
20 | + border-radius: 4px; | |
21 | + padding: 8px; | |
22 | + .mat-form-field.severity { | |
23 | + border-right: 1px groove rgba(0, 0, 0, 0.25); | |
24 | + padding-right: 8px; | |
25 | + } | |
26 | + } | |
27 | +} | |
17 | 28 | |
29 | +:host ::ng-deep { | |
30 | + .mat-form-field.severity { | |
31 | + .mat-form-field-infix { | |
32 | + width: 160px; | |
33 | + } | |
34 | + } | |
18 | 35 | } | ... | ... |
... | ... | @@ -150,15 +150,11 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, |
150 | 150 | } |
151 | 151 | |
152 | 152 | private updateModel() { |
153 | - if (this.createAlarmRulesFormGroup.valid) { | |
154 | - const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; | |
155 | - const createAlarmRules: {[severity: string]: AlarmRule} = {}; | |
156 | - value.forEach(v => { | |
157 | - createAlarmRules[v.severity] = v.alarmRule; | |
158 | - }); | |
159 | - this.propagateChange(createAlarmRules); | |
160 | - } else { | |
161 | - this.propagateChange(null); | |
162 | - } | |
153 | + const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; | |
154 | + const createAlarmRules: {[severity: string]: AlarmRule} = {}; | |
155 | + value.forEach(v => { | |
156 | + createAlarmRules[v.severity] = v.alarmRule; | |
157 | + }); | |
158 | + this.propagateChange(createAlarmRules); | |
163 | 159 | } |
164 | 160 | } | ... | ... |
... | ... | @@ -15,33 +15,80 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<fieldset [formGroup]="alarmFormGroup" class="fields-group tb-device-profile-alarm" fxFlex | |
19 | - style="position: relative;"> | |
20 | - <button *ngIf="!disabled" mat-icon-button color="primary" style="min-width: 40px;" | |
21 | - type="button" | |
22 | - style="position: absolute; top: 0; right: 0;" | |
23 | - (click)="removeAlarm.emit()" | |
24 | - matTooltip="{{ 'action.remove' | translate }}" | |
25 | - matTooltipPosition="above"> | |
26 | - <mat-icon>close</mat-icon> | |
27 | - </button> | |
28 | - <legend> | |
29 | - <mat-form-field floatLabel="always" | |
30 | - matTooltip="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"> | |
31 | - <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label> | |
32 | - <input required matInput formControlName="alarmType" placeholder="Enter alarm type"> | |
33 | - <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')"> | |
34 | - {{ 'device-profile.alarm-type-required' | translate }} | |
35 | - </mat-error> | |
36 | - <!--mat-hint innerHTML="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"></mat-hint--> | |
37 | - </mat-form-field> | |
38 | - </legend> | |
18 | +<mat-expansion-panel class="device-profile-alarm" fxFlex [formGroup]="alarmFormGroup" [(expanded)]="expanded"> | |
19 | + <mat-expansion-panel-header> | |
20 | + <div fxFlex fxLayout="row" fxLayoutAlign="start center"> | |
21 | + <mat-panel-title [fxShow]="!expanded"> | |
22 | + <div fxLayout="row" fxFlex fxLayoutAlign="start center"> | |
23 | + {{ alarmFormGroup.get('alarmType').value }} | |
24 | + </div> | |
25 | + </mat-panel-title> | |
26 | + <mat-form-field floatLabel="always" | |
27 | + style="width: 600px;" | |
28 | + [fxShow]="expanded" | |
29 | + (click)="!disabled ? $event.stopPropagation() : null;"> | |
30 | + <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label> | |
31 | + <input required matInput formControlName="alarmType" placeholder="Enter alarm type"> | |
32 | + <mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')"> | |
33 | + {{ 'device-profile.alarm-type-required' | translate }} | |
34 | + </mat-error> | |
35 | + <mat-hint *ngIf="!disabled" | |
36 | + innerHTML="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"></mat-hint> | |
37 | + </mat-form-field> | |
38 | + <span fxFlex></span> | |
39 | + <button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" | |
40 | + type="button" | |
41 | + (click)="removeAlarm.emit()" | |
42 | + matTooltip="{{ 'action.remove' | translate }}" | |
43 | + matTooltipPosition="above"> | |
44 | + <mat-icon>delete</mat-icon> | |
45 | + </button> | |
46 | + </div> | |
47 | + </mat-expansion-panel-header> | |
39 | 48 | <div fxFlex fxLayout="column"> |
40 | - <div translate class="tb-small" style="font-weight: 500;">device-profile.create-alarm-rules</div> | |
41 | - <mat-divider></mat-divider> | |
42 | - <tb-create-alarm-rules formControlName="createRules"> | |
49 | + <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.create-alarm-rules</div> | |
50 | + <tb-create-alarm-rules formControlName="createRules" style="padding-bottom: 16px;"> | |
43 | 51 | </tb-create-alarm-rules> |
44 | - <div translate class="tb-small" style="font-weight: 500;">device-profile.clear-alarm-rule</div> | |
45 | - <mat-divider></mat-divider> | |
52 | + <div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div> | |
53 | + <div fxFlex fxLayout="row" | |
54 | + [fxShow]="alarmFormGroup.get('clearRule').value" | |
55 | + fxLayoutGap="8px;" fxLayoutAlign="start center" style="padding-bottom: 8px;"> | |
56 | + <div class="clear-alarm-rule" fxFlex fxLayout="row"> | |
57 | + <tb-alarm-rule formControlName="clearRule" fxFlex> | |
58 | + </tb-alarm-rule> | |
59 | + </div> | |
60 | + <button *ngIf="!disabled" | |
61 | + mat-icon-button color="primary" style="min-width: 40px;" | |
62 | + type="button" | |
63 | + (click)="removeClearAlarmRule()" | |
64 | + matTooltip="{{ 'action.remove' | translate }}" | |
65 | + matTooltipPosition="above"> | |
66 | + <mat-icon>remove_circle_outline</mat-icon> | |
67 | + </button> | |
68 | + </div> | |
69 | + <div fxLayout="row" *ngIf="!disabled" | |
70 | + [fxShow]="!alarmFormGroup.get('clearRule').value"> | |
71 | + <button mat-stroked-button color="primary" | |
72 | + type="button" | |
73 | + (click)="addClearAlarmRule()" | |
74 | + matTooltip="{{ 'device-profile.add-clear-alarm-rule' | translate }}" | |
75 | + matTooltipPosition="above"> | |
76 | + <mat-icon>add_circle_outline</mat-icon> | |
77 | + {{ 'device-profile.add-clear-alarm-rule' | translate }} | |
78 | + </button> | |
79 | + </div> | |
46 | 80 | </div> |
47 | -</fieldset> | |
81 | + <mat-expansion-panel class="advanced-settings" [expanded]="false"> | |
82 | + <mat-expansion-panel-header> | |
83 | + <mat-panel-title> | |
84 | + <div fxFlex fxLayout="row" fxLayoutAlign="end center"> | |
85 | + <div class="tb-small" translate>device-profile.advanced-settings</div> | |
86 | + </div> | |
87 | + </mat-panel-title> | |
88 | + </mat-expansion-panel-header> | |
89 | + <mat-checkbox formControlName="propagate" style="padding-bottom: 16px;"> | |
90 | + {{ 'device-profile.propagate-alarm' | translate }} | |
91 | + </mat-checkbox> | |
92 | + <div>TODO: Propagate relation types</div> | |
93 | + </mat-expansion-panel> | |
94 | +</mat-expansion-panel> | ... | ... |
... | ... | @@ -13,35 +13,42 @@ |
13 | 13 | * See the License for the specific language governing permissions and |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | -@import '../scss/constants'; | |
17 | 16 | |
18 | 17 | :host { |
19 | 18 | display: block; |
20 | - .tb-device-profile-alarm { | |
21 | - &.mat-padding { | |
22 | - padding: 8px; | |
23 | - @media #{$mat-gt-sm} { | |
24 | - padding: 16px; | |
19 | + .clear-alarm-rule { | |
20 | + border: 1px groove rgba(0, 0, 0, .25); | |
21 | + border-radius: 4px; | |
22 | + padding: 8px; | |
23 | + } | |
24 | + .mat-expansion-panel { | |
25 | + box-shadow: none; | |
26 | + &.device-profile-alarm { | |
27 | + border: 1px groove rgba(0, 0, 0, .25); | |
28 | + .mat-expansion-panel-header { | |
29 | + padding: 0 24px 0 8px; | |
30 | + &.mat-expanded { | |
31 | + height: 80px; | |
32 | + } | |
25 | 33 | } |
26 | 34 | } |
27 | - } | |
28 | - a.mat-icon-button { | |
29 | - &:hover, &:focus { | |
30 | - border-bottom: none; | |
35 | + &.advanced-settings { | |
36 | + border: none; | |
37 | + padding: 0; | |
31 | 38 | } |
32 | 39 | } |
33 | - .fields-group { | |
34 | - padding: 8px; | |
35 | - margin: 10px 0; | |
36 | - border: 1px groove rgba(0, 0, 0, .25); | |
37 | - border-radius: 4px; | |
40 | +} | |
38 | 41 | |
39 | - legend { | |
40 | - padding-left: 8px; | |
41 | - padding-right: 8px; | |
42 | - margin-bottom: -30px; | |
43 | - .mat-form-field { | |
44 | - margin-bottom: 21px; | |
42 | +:host ::ng-deep { | |
43 | + .mat-expansion-panel { | |
44 | + &.device-profile-alarm { | |
45 | + .mat-expansion-panel-body { | |
46 | + padding: 0 8px; | |
47 | + } | |
48 | + } | |
49 | + &.advanced-settings { | |
50 | + .mat-expansion-panel-body { | |
51 | + padding: 0; | |
45 | 52 | } |
46 | 53 | } |
47 | 54 | } | ... | ... |
... | ... | @@ -19,17 +19,14 @@ import { |
19 | 19 | ControlValueAccessor, |
20 | 20 | FormBuilder, |
21 | 21 | FormControl, |
22 | - FormGroup, NG_VALIDATORS, | |
23 | - NG_VALUE_ACCESSOR, Validator, | |
22 | + FormGroup, | |
23 | + NG_VALIDATORS, | |
24 | + NG_VALUE_ACCESSOR, | |
25 | + Validator, | |
24 | 26 | Validators |
25 | 27 | } from '@angular/forms'; |
26 | -import { DeviceProfileAlarm } from '@shared/models/device.models'; | |
27 | -import { deepClone } from '@core/utils'; | |
28 | +import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models'; | |
28 | 29 | import { MatDialog } from '@angular/material/dialog'; |
29 | -import { | |
30 | - DeviceProfileAlarmDialogComponent, | |
31 | - DeviceProfileAlarmDialogData | |
32 | -} from './device-profile-alarm-dialog.component'; | |
33 | 30 | |
34 | 31 | @Component({ |
35 | 32 | selector: 'tb-device-profile-alarm', |
... | ... | @@ -56,6 +53,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit |
56 | 53 | @Output() |
57 | 54 | removeAlarm = new EventEmitter(); |
58 | 55 | |
56 | + expanded = false; | |
57 | + | |
59 | 58 | private modelValue: DeviceProfileAlarm; |
60 | 59 | |
61 | 60 | alarmFormGroup: FormGroup; |
... | ... | @@ -98,31 +97,24 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit |
98 | 97 | |
99 | 98 | writeValue(value: DeviceProfileAlarm): void { |
100 | 99 | this.modelValue = value; |
101 | - this.alarmFormGroup.reset(this.modelValue, {emitEvent: false}); | |
100 | + if (!this.modelValue.alarmType) { | |
101 | + this.expanded = true; | |
102 | + } | |
103 | + this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); | |
102 | 104 | } |
103 | 105 | |
104 | -/* openAlarm($event: Event) { | |
105 | - if ($event) { | |
106 | - $event.stopPropagation(); | |
107 | - } | |
108 | - this.dialog.open<DeviceProfileAlarmDialogComponent, DeviceProfileAlarmDialogData, | |
109 | - DeviceProfileAlarm>(DeviceProfileAlarmDialogComponent, { | |
110 | - disableClose: true, | |
111 | - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
112 | - data: { | |
113 | - isAdd: false, | |
114 | - alarm: this.disabled ? this.modelValue : deepClone(this.modelValue), | |
115 | - isReadOnly: this.disabled | |
116 | - } | |
117 | - }).afterClosed().subscribe( | |
118 | - (deviceProfileAlarm) => { | |
119 | - if (deviceProfileAlarm) { | |
120 | - this.modelValue = deviceProfileAlarm; | |
121 | - this.updateModel(); | |
122 | - } | |
106 | + public addClearAlarmRule() { | |
107 | + const clearAlarmRule: AlarmRule = { | |
108 | + condition: { | |
109 | + condition: [] | |
123 | 110 | } |
124 | - ); | |
125 | - } */ | |
111 | + }; | |
112 | + this.alarmFormGroup.patchValue({clearRule: clearAlarmRule}); | |
113 | + } | |
114 | + | |
115 | + public removeClearAlarmRule() { | |
116 | + this.alarmFormGroup.patchValue({clearRule: null}); | |
117 | + } | |
126 | 118 | |
127 | 119 | public validate(c: FormControl) { |
128 | 120 | return (this.alarmFormGroup.valid) ? null : { |
... | ... | @@ -133,12 +125,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit |
133 | 125 | } |
134 | 126 | |
135 | 127 | private updateModel() { |
136 | - if (this.alarmFormGroup.valid) { | |
137 | - const value = this.alarmFormGroup.value; | |
138 | - this.modelValue = {...this.modelValue, ...value}; | |
139 | - this.propagateChange(this.modelValue); | |
140 | - } else { | |
141 | - this.propagateChange(null); | |
142 | - } | |
128 | + const value = this.alarmFormGroup.value; | |
129 | + this.modelValue = {...this.modelValue, ...value}; | |
130 | + this.propagateChange(this.modelValue); | |
143 | 131 | } |
144 | 132 | } | ... | ... |
... | ... | @@ -17,8 +17,9 @@ |
17 | 17 | --> |
18 | 18 | <div fxLayout="column"> |
19 | 19 | <div class="tb-device-profile-alarms"> |
20 | - <div *ngFor="let alarmControl of alarmsFormArray().controls; let $index = index; last as isLast;" | |
21 | - fxLayout="column"> | |
20 | + <div *ngFor="let alarmControl of alarmsFormArray().controls; trackBy: trackByAlarm; | |
21 | + let $index = index; last as isLast;" | |
22 | + fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}"> | |
22 | 23 | <tb-device-profile-alarm [formControl]="alarmControl" |
23 | 24 | (removeAlarm)="removeAlarm($index)"> |
24 | 25 | </tb-device-profile-alarm> | ... | ... |
... | ... | @@ -19,9 +19,12 @@ import { |
19 | 19 | AbstractControl, |
20 | 20 | ControlValueAccessor, |
21 | 21 | FormArray, |
22 | - FormBuilder, FormControl, | |
23 | - FormGroup, NG_VALIDATORS, | |
24 | - NG_VALUE_ACCESSOR, Validator, | |
22 | + FormBuilder, | |
23 | + FormControl, | |
24 | + FormGroup, | |
25 | + NG_VALIDATORS, | |
26 | + NG_VALUE_ACCESSOR, | |
27 | + Validator, | |
25 | 28 | Validators |
26 | 29 | } from '@angular/forms'; |
27 | 30 | import { Store } from '@ngrx/store'; |
... | ... | @@ -30,10 +33,6 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
30 | 33 | import { DeviceProfileAlarm } from '@shared/models/device.models'; |
31 | 34 | import { guid } from '@core/utils'; |
32 | 35 | import { Subscription } from 'rxjs'; |
33 | -import { | |
34 | - DeviceProfileAlarmDialogComponent, | |
35 | - DeviceProfileAlarmDialogData | |
36 | -} from './device-profile-alarm-dialog.component'; | |
37 | 36 | import { MatDialog } from '@angular/material/dialog'; |
38 | 37 | |
39 | 38 | @Component({ |
... | ... | @@ -125,6 +124,14 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni |
125 | 124 | }); |
126 | 125 | } |
127 | 126 | |
127 | + public trackByAlarm(index: number, alarmControl: AbstractControl): string { | |
128 | + if (alarmControl) { | |
129 | + return alarmControl.value.id; | |
130 | + } else { | |
131 | + return null; | |
132 | + } | |
133 | + } | |
134 | + | |
128 | 135 | public removeAlarm(index: number) { |
129 | 136 | (this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray).removeAt(index); |
130 | 137 | } |
... | ... | @@ -144,22 +151,6 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni |
144 | 151 | const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; |
145 | 152 | alarmsArray.push(this.fb.control(alarm, [Validators.required])); |
146 | 153 | this.deviceProfileAlarmsFormGroup.updateValueAndValidity(); |
147 | - | |
148 | -/* this.dialog.open<DeviceProfileAlarmDialogComponent, DeviceProfileAlarmDialogData, | |
149 | - DeviceProfileAlarm>(DeviceProfileAlarmDialogComponent, { | |
150 | - disableClose: true, | |
151 | - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
152 | - data: { | |
153 | - isAdd: true, | |
154 | - alarm, | |
155 | - isReadOnly: false | |
156 | - } | |
157 | - }).afterClosed().subscribe( | |
158 | - (deviceProfileAlarm) => { | |
159 | - if (deviceProfileAlarm) { | |
160 | - } | |
161 | - } | |
162 | - ); */ | |
163 | 154 | } |
164 | 155 | |
165 | 156 | public validate(c: FormControl) { |
... | ... | @@ -171,11 +162,11 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni |
171 | 162 | } |
172 | 163 | |
173 | 164 | private updateModel() { |
174 | - if (this.deviceProfileAlarmsFormGroup.valid) { | |
165 | +// if (this.deviceProfileAlarmsFormGroup.valid) { | |
175 | 166 | const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value; |
176 | 167 | this.propagateChange(alarms); |
177 | - } else { | |
168 | + /* } else { | |
178 | 169 | this.propagateChange(null); |
179 | - } | |
170 | + } */ | |
180 | 171 | } |
181 | 172 | } | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<form (ngSubmit)="save()" style="min-width: 600px;"> | |
18 | +<form (ngSubmit)="save()" style="min-width: 1000px;"> | |
19 | 19 | <mat-toolbar color="primary"> |
20 | 20 | <h2>{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}</h2> |
21 | 21 | <span fxFlex></span> | ... | ... |
... | ... | @@ -52,7 +52,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon |
52 | 52 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE_PROFILE); |
53 | 53 | this.config.entityResources = entityTypeResources.get(EntityType.DEVICE_PROFILE); |
54 | 54 | |
55 | - this.config.addDialogStyle = {width: '600px'}; | |
55 | + this.config.addDialogStyle = {width: '1000px'}; | |
56 | 56 | |
57 | 57 | this.config.columns.push( |
58 | 58 | new DateEntityTableColumn<DeviceProfile>('createdTime', 'common.created-time', this.datePipe, '150px'), | ... | ... |
... | ... | @@ -148,12 +148,16 @@ export function createDefaultFilterPredicateInfo(valueType: EntityKeyValueType, |
148 | 148 | const predicate = createDefaultFilterPredicate(valueType, complex); |
149 | 149 | return { |
150 | 150 | keyFilterPredicate: predicate, |
151 | - userInfo: { | |
152 | - editable: true, | |
153 | - label: '', | |
154 | - autogeneratedLabel: true, | |
155 | - order: 0 | |
156 | - } | |
151 | + userInfo: createDefaultFilterPredicateUserInfo() | |
152 | + }; | |
153 | +} | |
154 | + | |
155 | +export function createDefaultFilterPredicateUserInfo(): KeyFilterPredicateUserInfo { | |
156 | + return { | |
157 | + editable: true, | |
158 | + label: '', | |
159 | + autogeneratedLabel: true, | |
160 | + order: 0 | |
157 | 161 | }; |
158 | 162 | } |
159 | 163 | |
... | ... | @@ -334,6 +338,7 @@ export interface KeyFilterPredicateInfo { |
334 | 338 | |
335 | 339 | export interface KeyFilter { |
336 | 340 | key: EntityKey; |
341 | + valueType: EntityKeyValueType; | |
337 | 342 | predicate: KeyFilterPredicate; |
338 | 343 | } |
339 | 344 | |
... | ... | @@ -353,6 +358,45 @@ export interface FiltersInfo { |
353 | 358 | datasourceFilters: {[datasourceIndex: number]: FilterInfo}; |
354 | 359 | } |
355 | 360 | |
361 | +export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> { | |
362 | + const keyFilters: Array<KeyFilter> = []; | |
363 | + for (const keyFilterInfo of keyFilterInfos) { | |
364 | + const key = keyFilterInfo.key; | |
365 | + for (const predicate of keyFilterInfo.predicates) { | |
366 | + const keyFilter: KeyFilter = { | |
367 | + key, | |
368 | + valueType: keyFilterInfo.valueType, | |
369 | + predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) | |
370 | + }; | |
371 | + keyFilters.push(keyFilter); | |
372 | + } | |
373 | + } | |
374 | + return keyFilters; | |
375 | +} | |
376 | + | |
377 | +export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> { | |
378 | + const keyFilterInfos: Array<KeyFilterInfo> = []; | |
379 | + const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {}; | |
380 | + for (const keyFilter of keyFilters) { | |
381 | + const key = keyFilter.key; | |
382 | + const infoKey = key.key + key.type + keyFilter.valueType; | |
383 | + let keyFilterInfo = keyFilterInfoMap[infoKey]; | |
384 | + if (!keyFilterInfo) { | |
385 | + keyFilterInfo = { | |
386 | + key, | |
387 | + valueType: keyFilter.valueType, | |
388 | + predicates: [] | |
389 | + }; | |
390 | + keyFilterInfoMap[infoKey] = keyFilterInfo; | |
391 | + keyFilterInfos.push(keyFilterInfo); | |
392 | + } | |
393 | + if (keyFilter.predicate) { | |
394 | + keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate)); | |
395 | + } | |
396 | + } | |
397 | + return keyFilterInfos; | |
398 | +} | |
399 | + | |
356 | 400 | export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> { |
357 | 401 | const keyFilterInfos = filter.keyFilters; |
358 | 402 | const keyFilters: Array<KeyFilter> = []; |
... | ... | @@ -361,6 +405,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> { |
361 | 405 | for (const predicate of keyFilterInfo.predicates) { |
362 | 406 | const keyFilter: KeyFilter = { |
363 | 407 | key, |
408 | + valueType: keyFilterInfo.valueType, | |
364 | 409 | predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate) |
365 | 410 | }; |
366 | 411 | keyFilters.push(keyFilter); |
... | ... | @@ -383,6 +428,26 @@ export function keyFilterPredicateInfoToKeyFilterPredicate(keyFilterPredicateInf |
383 | 428 | return keyFilterPredicate; |
384 | 429 | } |
385 | 430 | |
431 | +export function keyFilterPredicateToKeyFilterPredicateInfo(keyFilterPredicate: KeyFilterPredicate): KeyFilterPredicateInfo { | |
432 | + const keyFilterPredicateInfo: KeyFilterPredicateInfo = { | |
433 | + keyFilterPredicate: null, | |
434 | + userInfo: null | |
435 | + }; | |
436 | + if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) { | |
437 | + const complexPredicate = keyFilterPredicate as ComplexFilterPredicate; | |
438 | + const predicateInfos = complexPredicate.predicates.map( | |
439 | + predicate => keyFilterPredicateToKeyFilterPredicateInfo(predicate)); | |
440 | + keyFilterPredicateInfo.keyFilterPredicate = { | |
441 | + predicates: predicateInfos, | |
442 | + operation: complexPredicate.operation, | |
443 | + type: FilterPredicateType.COMPLEX | |
444 | + } as ComplexFilterPredicateInfo; | |
445 | + } else { | |
446 | + keyFilterPredicateInfo.keyFilterPredicate = keyFilterPredicate; | |
447 | + } | |
448 | + return keyFilterPredicateInfo; | |
449 | +} | |
450 | + | |
386 | 451 | export function isFilterEditable(filter: FilterInfo): boolean { |
387 | 452 | if (filter.editable) { |
388 | 453 | return filter.keyFilters.some(value => isKeyFilterInfoEditable(value)); | ... | ... |
... | ... | @@ -815,8 +815,21 @@ |
815 | 815 | "create-alarm-rules": "Create alarm rules", |
816 | 816 | "clear-alarm-rule": "Clear alarm rule", |
817 | 817 | "add-create-alarm-rule": "Add create alarm rule", |
818 | + "add-clear-alarm-rule": "Add clear alarm rule", | |
818 | 819 | "select-alarm-severity": "Select alarm severity", |
819 | - "alarm-severity-required": "Alarm severity is required." | |
820 | + "alarm-severity-required": "Alarm severity is required.", | |
821 | + "condition-duration": "Condition duration", | |
822 | + "condition-duration-value": "Duration value", | |
823 | + "condition-duration-time-unit": "Time unit", | |
824 | + "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", | |
825 | + "condition-duration-value-required": "Duration value is required.", | |
826 | + "condition-duration-time-unit-required": "Time unit is required.", | |
827 | + "advanced-settings": "Advanced settings", | |
828 | + "propagate-alarm": "Propagate alarm", | |
829 | + "alarm-details": "Alarm details", | |
830 | + "alarm-rule-condition": "Alarm rule condition", | |
831 | + "enter-alarm-rule-condition-prompt": "Please add alarm rule condition", | |
832 | + "edit-alarm-rule-condition": "Edit alarm rule condition" | |
820 | 833 | }, |
821 | 834 | "dialog": { |
822 | 835 | "close": "Close dialog" |
... | ... | @@ -1286,6 +1299,7 @@ |
1286 | 1299 | "complex-filter": "Complex filter", |
1287 | 1300 | "edit-complex-filter": "Edit complex filter", |
1288 | 1301 | "edit-filter-user-params": "Edit filter predicate user parameters", |
1302 | + "filter-user-params": "Filter predicate user parameters", | |
1289 | 1303 | "user-parameters": "User parameters", |
1290 | 1304 | "display-label": "Label to display", |
1291 | 1305 | "autogenerated-label": "Auto generate label", | ... | ... |
... | ... | @@ -563,7 +563,7 @@ mat-label { |
563 | 563 | } |
564 | 564 | } |
565 | 565 | |
566 | - mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell { | |
566 | + mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell, .mat-expansion-panel-header { | |
567 | 567 | button.mat-icon-button { |
568 | 568 | mat-icon { |
569 | 569 | color: rgba(0, 0, 0, .54); | ... | ... |