UI: Device profile alarm rules

This commit is contained in:
Igor Kulikov 2020-09-10 19:37:14 +03:00
parent 3b55ebe5d4
commit d9321b4816
45 changed files with 750 additions and 376 deletions

View File

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.query;
public enum EntityKeyValueType {
STRING,
NUMERIC,
BOOLEAN,
DATE_TIME
}

View File

@ -21,6 +21,7 @@ import lombok.Data;
public class KeyFilter {
private EntityKey key;
private EntityKeyValueType valueType;
private KeyFilterPredicate predicate;
}

View File

@ -62,6 +62,7 @@ import {
entityInfoFields,
EntityKey,
EntityKeyType,
EntityKeyValueType,
FilterPredicateType,
singleEntityDataPageLink,
StringOperation
@ -399,6 +400,7 @@ export class EntityService {
keyFilters: searchText && searchText.length ? [
{
key: nameField,
valueType: EntityKeyValueType.STRING,
predicate: {
type: FilterPredicateType.STRING,
operation: StringOperation.STARTS_WITH,
@ -593,10 +595,10 @@ export class EntityService {
return entityTypes;
}
private getEntityFieldKeys (entityType: EntityType, searchText: string): Array<string> {
private getEntityFieldKeys(entityType: EntityType, searchText: string): Array<string> {
const entityFieldKeys: string[] = [entityFields.createdTime.keyName];
const query = searchText.toLowerCase();
switch(entityType) {
switch (entityType) {
case EntityType.USER:
entityFieldKeys.push(entityFields.name.keyName);
entityFieldKeys.push(entityFields.email.keyName);
@ -863,7 +865,7 @@ export class EntityService {
const tasks: Observable<any>[] = [];
const result: Device | Asset = entity as (Device | Asset);
const additionalInfo = result.additionalInfo || {};
if(result.label !== entityData.label ||
if (result.label !== entityData.label ||
result.type !== entityData.type ||
additionalInfo.description !== entityData.description ||
(result.id.entityType === EntityType.DEVICE && (additionalInfo.gateway !== entityData.gateway)) ) {

View File

@ -251,13 +251,14 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
}
onToggleEditMode(isEdit: boolean) {
this.isEdit = isEdit;
if (!this.isEdit) {
if (!isEdit) {
this.entityComponent.entity = this.entity;
if (this.entityTabsComponent) {
this.entityTabsComponent.entity = this.entity;
}
this.isEdit = isEdit;
} else {
this.isEdit = isEdit;
this.editingEntity = deepClone(this.entity);
this.entityComponent.entity = this.editingEntity;
if (this.entityTabsComponent) {

View File

@ -65,7 +65,6 @@ export abstract class EntityComponent<T extends BaseData<HasId>,
set entity(entity: T) {
this.entityValue = entity;
if (this.entityForm) {
this.entityForm.reset(undefined, {emitEvent: false});
this.entityForm.markAsPristine();
this.updateForm(entity);
}

View File

@ -37,6 +37,7 @@
</mat-form-field>
<tb-filter-predicate-list
[valueType]="data.valueType"
[displayUserParameters]="data.displayUserParameters"
[operation]="complexFilterFormGroup.get('operation').value"
[key]="data.key"
formControlName="predicates">
@ -45,6 +46,7 @@
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
@ -54,7 +56,7 @@
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

View File

@ -32,9 +32,10 @@ import {
export interface ComplexFilterPredicateDialogData {
complexPredicate: ComplexFilterPredicateInfo;
key: string;
disabled: boolean;
readonly: boolean;
isAdd: boolean;
valueType: EntityKeyValueType;
displayUserParameters: boolean;
}
@Component({
@ -73,6 +74,9 @@ export class ComplexFilterPredicateDialogComponent extends
predicates: [this.data.complexPredicate.predicates, [Validators.required]]
}
);
if (this.data.readonly) {
this.complexFilterFormGroup.disable({emitEvent: false});
}
}
ngOnInit(): void {

View File

@ -19,11 +19,10 @@
<mat-label translate>filter.complex-filter</mat-label>
<button mat-icon-button color="primary"
class="tb-mat-32"
[fxShow]="!disabled"
type="button"
(click)="openComplexFilterDialog()"
matTooltip="{{ 'filter.edit-complex-filter' | translate }}"
matTooltip="{{ (disabled ? 'filter.complex-filter' : 'filter.edit-complex-filter') | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
<mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon>
</button>
</div>

View File

@ -48,6 +48,8 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
@Input() key: string;
@Input() displayUserParameters = true;
private propagateChange = null;
private complexFilterPredicate: ComplexFilterPredicateInfo;
@ -79,11 +81,12 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: deepClone(this.complexFilterPredicate),
disabled: this.disabled,
complexPredicate: this.disabled ? this.complexFilterPredicate : deepClone(this.complexFilterPredicate),
readonly: this.disabled,
valueType: this.valueType,
isAdd: false,
key: this.key
key: this.key,
displayUserParameters: this.displayUserParameters
}
}).afterClosed().subscribe(
(result) => {

View File

@ -33,7 +33,8 @@
</div>
<label fxFlex="60" translate class="tb-title no-padding">filter.value</label>
</div>
<label translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
<label *ngIf="displayUserParameters"
translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
<span [fxShow]="!disabled" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
@ -50,6 +51,7 @@
<tb-filter-predicate
fxFlex
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"
[key]="key"
[formControl]="predicateControl">
</tb-filter-predicate>

View File

@ -62,6 +62,8 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
@Input() operation: ComplexOperation = ComplexOperation.AND;
@Input() displayUserParameters = true;
filterListFormGroup: FormGroup;
valueTypeEnum = EntityKeyValueType;
@ -150,10 +152,11 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: predicate.keyFilterPredicate as ComplexFilterPredicateInfo,
disabled: this.disabled,
readonly: this.disabled,
valueType: this.valueType,
key: this.key,
isAdd: true
isAdd: true,
displayUserParameters: this.displayUserParameters
}
}).afterClosed().pipe(
map((result) => {

View File

@ -35,11 +35,12 @@
<tb-complex-filter-predicate
[key]="key"
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"
formControlName="predicate">
</tb-complex-filter-predicate>
</ng-template>
</div>
<tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX"
<tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX && displayUserParameters"
style="width: 60px;"
fxLayout="row" fxLayoutAlign="center"
[valueType]="valueType"

View File

@ -41,6 +41,8 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
@Input() key: string;
@Input() displayUserParameters = true;
filterPredicateFormGroup: FormGroup;
type: FilterPredicateType;

View File

@ -17,7 +17,7 @@
-->
<form [formGroup]="filterUserInfoFormGroup" (ngSubmit)="save()" style="width: 500px;">
<mat-toolbar color="primary">
<h2 translate>filter.edit-filter-user-params</h2>
<h2>{{(data.readonly ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate}}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -47,6 +47,7 @@
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }}
@ -56,7 +57,7 @@
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

View File

@ -23,7 +23,7 @@ import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Valida
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
BooleanOperation,
BooleanOperation, createDefaultFilterPredicateUserInfo,
EntityKeyValueType, generateUserFilterValueLabel,
KeyFilterPredicateUserInfo, NumericOperation,
StringOperation
@ -35,6 +35,7 @@ export interface FilterUserInfoDialogData {
valueType: EntityKeyValueType;
operation: StringOperation | BooleanOperation | NumericOperation;
keyFilterPredicateUserInfo: KeyFilterPredicateUserInfo;
readonly: boolean;
}
@Component({
@ -60,18 +61,24 @@ export class FilterUserInfoDialogComponent extends
private translate: TranslateService) {
super(store, router, dialogRef);
const userInfo: KeyFilterPredicateUserInfo = this.data.keyFilterPredicateUserInfo || createDefaultFilterPredicateUserInfo();
this.filterUserInfoFormGroup = this.fb.group(
{
editable: [this.data.keyFilterPredicateUserInfo.editable],
label: [this.data.keyFilterPredicateUserInfo.label],
autogeneratedLabel: [this.data.keyFilterPredicateUserInfo.autogeneratedLabel],
order: [this.data.keyFilterPredicateUserInfo.order]
editable: [userInfo.editable],
label: [userInfo.label],
autogeneratedLabel: [userInfo.autogeneratedLabel],
order: [userInfo.order]
}
);
this.onAutogeneratedLabelChange();
this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => {
this.onAutogeneratedLabelChange();
});
if (!this.data.readonly) {
this.filterUserInfoFormGroup.get('autogeneratedLabel').valueChanges.subscribe(() => {
this.onAutogeneratedLabelChange();
});
} else {
this.filterUserInfoFormGroup.disable({emitEvent: false});
}
}
private onAutogeneratedLabelChange() {

View File

@ -17,10 +17,9 @@
-->
<button mat-icon-button color="primary"
class="tb-mat-32"
[fxShow]="!disabled"
type="button"
(click)="openFilterUserInfoDialog()"
matTooltip="{{ 'filter.edit-filter-user-params' | translate }}"
matTooltip="{{ (disabled ? 'filter.filter-user-params' : 'filter.edit-filter-user-params') | translate }}"
matTooltipPosition="above">
<mat-icon>settings</mat-icon>
</button>

View File

@ -76,7 +76,7 @@ export class FilterUserInfoComponent implements ControlValueAccessor, OnInit {
this.keyFilterPredicateUserInfo = keyFilterPredicateUserInfo;
}
private openFilterUserInfoDialog() {
public openFilterUserInfoDialog() {
this.dialog.open<FilterUserInfoDialogComponent, FilterUserInfoDialogData,
KeyFilterPredicateUserInfo>(FilterUserInfoDialogComponent, {
disableClose: true,
@ -85,7 +85,8 @@ export class FilterUserInfoComponent implements ControlValueAccessor, OnInit {
keyFilterPredicateUserInfo: deepClone(this.keyFilterPredicateUserInfo),
valueType: this.valueType,
key: this.key,
operation: this.operation
operation: this.operation,
readonly: this.disabled
}
}).afterClosed().subscribe(
(result) => {

View File

@ -17,7 +17,7 @@
-->
<form [formGroup]="keyFilterFormGroup" (ngSubmit)="save()" style="width: 900px;">
<mat-toolbar color="primary">
<h2>{{(data.isAdd ? 'filter.add-key-filter' : 'filter.edit-key-filter') | translate}}</h2>
<h2>{{(data.isAdd ? 'filter.add-key-filter' : (data.readonly ? 'filter.key-filter' : 'filter.edit-key-filter')) | translate}}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
@ -70,6 +70,7 @@
</mat-form-field>
</section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
[displayUserParameters]="data.displayUserParameters"
[valueType]="keyFilterFormGroup.get('valueType').value"
[key]="keyFilterFormGroup.get('key.key').value"
formControlName="predicates">
@ -79,6 +80,7 @@
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="submit"
*ngIf="!data.readonly"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
@ -87,7 +89,7 @@
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

View File

@ -39,6 +39,9 @@ import { filter, map, startWith } from 'rxjs/operators';
export interface KeyFilterDialogData {
keyFilter: KeyFilterInfo;
isAdd: boolean;
displayUserParameters: boolean;
readonly: boolean;
telemetryKeysOnly: boolean;
}
@Component({
@ -53,7 +56,10 @@ export class KeyFilterDialogComponent extends
keyFilterFormGroup: FormGroup;
entityKeyTypes = [EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES];
entityKeyTypes =
this.data.telemetryKeysOnly ?
[EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES] :
[EntityKeyType.ENTITY_FIELD, EntityKeyType.ATTRIBUTE, EntityKeyType.TIME_SERIES];
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
@ -95,32 +101,37 @@ export class KeyFilterDialogComponent extends
predicates: [this.data.keyFilter.predicates, [Validators.required]]
}
);
this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => {
const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
if (prevValue && prevValue !== valueType && predicates && predicates.length) {
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
this.translate.instant('filter.key-value-type-change-message')).subscribe(
(result) => {
if (result) {
this.keyFilterFormGroup.get('predicates').setValue([]);
} else {
this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false});
}
}
);
}
});
this.keyFilterFormGroup.get('key.key').valueChanges.pipe(
filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName))
).subscribe((keyName: string) => {
const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING;
if (prevValueType !== newValueType) {
this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false});
}
});
if (!this.data.readonly) {
this.keyFilterFormGroup.get('valueType').valueChanges.subscribe((valueType: EntityKeyValueType) => {
const prevValue: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const predicates: KeyFilterPredicate[] = this.keyFilterFormGroup.get('predicates').value;
if (prevValue && prevValue !== valueType && predicates && predicates.length) {
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
this.translate.instant('filter.key-value-type-change-message')).subscribe(
(result) => {
if (result) {
this.keyFilterFormGroup.get('predicates').setValue([]);
} else {
this.keyFilterFormGroup.get('valueType').setValue(prevValue, {emitEvent: false});
}
}
);
}
});
this.keyFilterFormGroup.get('key.key').valueChanges.pipe(
filter((keyName) => this.keyFilterFormGroup.get('key.type').value === this.entityField && this.entityFields.hasOwnProperty(keyName))
).subscribe((keyName: string) => {
const prevValueType: EntityKeyValueType = this.keyFilterFormGroup.value.valueType;
const newValueType = this.entityFields[keyName]?.time ? EntityKeyValueType.DATE_TIME : EntityKeyValueType.STRING;
if (prevValueType !== newValueType) {
this.keyFilterFormGroup.get('valueType').patchValue(newValueType, {emitEvent: false});
}
});
} else {
this.keyFilterFormGroup.disable({emitEvent: false});
}
this.entityFields = entityFields;
this.entityFieldsList = Object.values(entityFields).map(entityField => entityField.keyName).sort();

View File

@ -46,9 +46,9 @@
<button mat-icon-button color="primary"
type="button"
(click)="editKeyFilter($index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltip="{{ (disabled ? 'filter.key-filter' : 'filter.edit-key-filter') | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
<mat-icon>{{disabled ? 'more_vert' : 'edit'}}</mat-icon>
</button>
<button mat-icon-button color="primary"
[fxShow]="!disabled"

View File

@ -46,6 +46,10 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
@Input() disabled: boolean;
@Input() displayUserParameters = true;
@Input() telemetryKeysOnly = false;
keyFilterListFormGroup: FormGroup;
entityKeyTypeTranslations = entityKeyTypeTranslationMap;
@ -147,8 +151,11 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
keyFilter: keyFilter ? deepClone(keyFilter): null,
isAdd
keyFilter: keyFilter ? (this.disabled ? keyFilter : deepClone(keyFilter)) : null,
isAdd,
readonly: this.disabled,
displayUserParameters: this.displayUserParameters,
telemetryKeysOnly: this.telemetryKeysOnly
}
}).afterClosed();
}

View File

@ -100,10 +100,10 @@ import { MqttDeviceProfileTransportConfigurationComponent } from './profile/devi
import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/device/lwm2m-device-profile-transport-configuration.component';
import { DeviceProfileAlarmsComponent } from './profile/alarm/device-profile-alarms.component';
import { DeviceProfileAlarmComponent } from './profile/alarm/device-profile-alarm.component';
import { DeviceProfileAlarmDialogComponent } from './profile/alarm/device-profile-alarm-dialog.component';
import { CreateAlarmRulesComponent } from './profile/alarm/create-alarm-rules.component';
import { AlarmRuleComponent } from './profile/alarm/alarm-rule.component';
import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-condition.component';
import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-key-filters-dialog.component';
@NgModule({
declarations:
@ -184,9 +184,9 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleConditionComponent,
DeviceProfileAlarmComponent,
DeviceProfileAlarmDialogComponent,
DeviceProfileAlarmsComponent,
DeviceProfileDataComponent,
DeviceProfileComponent,
@ -260,9 +260,9 @@ import { AlarmRuleConditionComponent } from './profile/alarm/alarm-rule-conditio
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleConditionComponent,
DeviceProfileAlarmComponent,
DeviceProfileAlarmDialogComponent,
DeviceProfileAlarmsComponent,
DeviceProfileDataComponent,
DeviceProfileComponent,

View File

@ -15,5 +15,16 @@
limitations under the License.
-->
<div fxLayout="row" [formGroup]="alarmRuleConditionFormGroup">
</div>
<mat-form-field class="mat-block" (click)="openFilterDialog($event)" floatLabel="always" hideRequiredMarker>
<mat-label></mat-label>
<input readonly
required matInput [formControl]="alarmRuleConditionControl"
placeholder="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
<a matSuffix mat-icon-button color="primary"
type="button"
(click)="openFilterDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'more_vert' : 'edit' }}</mat-icon>
</a>
</mat-form-field>

View File

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
a.mat-icon-button {
&:hover, &:focus {
border-bottom: none;
}
}
}

View File

@ -19,19 +19,22 @@ import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
Validator
} from '@angular/forms';
import { AlarmCondition } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { KeyFilter } from '@shared/models/query/query.models';
import { deepClone } from '@core/utils';
import {
AlarmRuleKeyFiltersDialogComponent,
AlarmRuleKeyFiltersDialogData
} from './alarm-rule-key-filters-dialog.component';
@Component({
selector: 'tb-alarm-rule-condition',
templateUrl: './alarm-rule-condition.component.html',
styleUrls: [],
styleUrls: ['./alarm-rule-condition.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -50,9 +53,9 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
@Input()
disabled: boolean;
private modelValue: AlarmCondition;
alarmRuleConditionControl: FormControl;
alarmRuleConditionFormGroup: FormGroup;
private modelValue: Array<KeyFilter>;
private propagateChange = (v: any) => { };
@ -68,45 +71,56 @@ export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit
}
ngOnInit() {
this.alarmRuleConditionFormGroup = this.fb.group({
condition: [null, Validators.required],
durationUnit: [null],
durationValue: [null]
});
this.alarmRuleConditionFormGroup.valueChanges.subscribe(() => {
this.updateModel();
});
this.alarmRuleConditionControl = this.fb.control(null);
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmRuleConditionFormGroup.disable({emitEvent: false});
} else {
this.alarmRuleConditionFormGroup.enable({emitEvent: false});
}
}
writeValue(value: AlarmCondition): void {
writeValue(value: Array<KeyFilter>): void {
this.modelValue = value;
this.alarmRuleConditionFormGroup.reset(this.modelValue, {emitEvent: false});
this.updateConditionInfo();
}
public validate(c: FormControl) {
return (this.alarmRuleConditionFormGroup.valid) ? null : {
return (this.modelValue && this.modelValue.length) ? null : {
alarmRuleCondition: {
valid: false,
},
};
}
private updateModel() {
if (this.alarmRuleConditionFormGroup.valid) {
const value = this.alarmRuleConditionFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
public openFilterDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<AlarmRuleKeyFiltersDialogComponent, AlarmRuleKeyFiltersDialogData,
Array<KeyFilter>>(AlarmRuleKeyFiltersDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
keyFilters: this.disabled ? this.modelValue : deepClone(this.modelValue)
}
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue = result;
this.updateModel();
}
});
}
private updateConditionInfo() {
if (this.modelValue && this.modelValue.length) {
this.alarmRuleConditionControl.patchValue('Condition set');
} else {
this.propagateChange(null);
this.alarmRuleConditionControl.patchValue(null);
}
}
private updateModel() {
this.updateConditionInfo();
this.propagateChange(this.modelValue);
}
}

View File

@ -0,0 +1,55 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="keyFiltersFormGroup" (ngSubmit)="save()" style="width: 700px;">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'device-profile.alarm-rule-condition' : 'device-profile.edit-alarm-rule-condition') | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-key-filter-list
[displayUserParameters]="false"
[telemetryKeysOnly]="true"
formControlName="keyFilters">
</tb-key-filter-list>
</div>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || keyFiltersFormGroup.invalid || !keyFiltersFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

View File

@ -14,68 +14,58 @@
/// limitations under the License.
///
import {
Component,
Inject,
OnInit,
SkipSelf
} from '@angular/core';
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { DeviceProfileAlarm } from '@shared/models/device.models';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { KeyFilter, keyFilterInfosToKeyFilters, keyFiltersToKeyFilterInfos } from '@shared/models/query/query.models';
export interface DeviceProfileAlarmDialogData {
alarm: DeviceProfileAlarm;
isAdd: boolean;
isReadOnly: boolean;
export interface AlarmRuleKeyFiltersDialogData {
readonly: boolean;
keyFilters: Array<KeyFilter>;
}
@Component({
selector: 'tb-device-profile-alarm-dialog',
templateUrl: './device-profile-alarm-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileAlarmDialogComponent}],
selector: 'tb-alarm-rule-key-filters-dialog',
templateUrl: './alarm-rule-key-filters-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleKeyFiltersDialogComponent}],
styleUrls: []
})
export class DeviceProfileAlarmDialogComponent extends
DialogComponent<DeviceProfileAlarmDialogComponent, DeviceProfileAlarm> implements OnInit, ErrorStateMatcher {
export class AlarmRuleKeyFiltersDialogComponent extends DialogComponent<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>
implements OnInit, ErrorStateMatcher {
alarmFormGroup: FormGroup;
readonly = this.data.readonly;
keyFilters = this.data.keyFilters;
isReadOnly = this.data.isReadOnly;
alarm = this.data.alarm;
isAdd = this.data.isAdd;
keyFiltersFormGroup: FormGroup;
submitted = false;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: DeviceProfileAlarmDialogData,
public dialogRef: MatDialogRef<DeviceProfileAlarmDialogComponent, DeviceProfileAlarm>,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleKeyFiltersDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public fb: FormBuilder) {
public dialogRef: MatDialogRef<AlarmRuleKeyFiltersDialogComponent, Array<KeyFilter>>,
private fb: FormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef);
this.isAdd = this.data.isAdd;
this.alarm = this.data.alarm;
this.keyFiltersFormGroup = this.fb.group({
keyFilters: [keyFiltersToKeyFilterInfos(this.keyFilters), Validators.required]
});
if (this.readonly) {
this.keyFiltersFormGroup.disable({emitEvent: false});
}
}
ngOnInit(): void {
this.alarmFormGroup = this.fb.group({
id: [null, Validators.required],
alarmType: [null, Validators.required],
createRules: [null],
clearRule: [null],
propagate: [null],
propagateRelationTypes: [null]
});
this.alarmFormGroup.reset(this.alarm, {emitEvent: false});
if (this.isReadOnly) {
this.alarmFormGroup.disable({emitEvent: false});
}
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -90,10 +80,7 @@ export class DeviceProfileAlarmDialogComponent extends
save(): void {
this.submitted = true;
if (this.alarmFormGroup.valid) {
this.alarm = {...this.alarm, ...this.alarmFormGroup.value};
this.dialogRef.close(this.alarm);
}
this.keyFilters = keyFilterInfosToKeyFilters(this.keyFiltersFormGroup.get('keyFilters').value);
this.dialogRef.close(this.keyFilters);
}
}

View File

@ -15,8 +15,71 @@
limitations under the License.
-->
<div fxLayout="row" [formGroup]="alarmRuleFormGroup">
<tb-alarm-rule-condition
formControlName="condition">
</tb-alarm-rule-condition>
<div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<div formGroupName="condition" fxLayout="row" fxLayoutAlign="start" fxLayoutGap="8px" fxFlex>
<div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label class="tb-small" translate>device-profile.alarm-rule-condition</label>
</div>
<tb-alarm-rule-condition fxFlex
formControlName="condition">
</tb-alarm-rule-condition>
</div>
<div fxLayout="column" style="min-width: 250px;">
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<label fxFlex class="tb-small" translate>device-profile.condition-duration</label>
<mat-slide-toggle [disabled]="disabled"
[ngModelOptions]="{standalone: true}"
(ngModelChange)="enableDurationChanged($event)"
[ngModel]="enableDuration">
</mat-slide-toggle>
</div>
<div fxLayout="row" fxLayoutGap="8px" [fxShow]="enableDuration">
<mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<input type="number"
[required]="enableDuration"
step="1"
min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="durationValue">
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationValue').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block duration-unit-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="durationUnit"
[required]="enableDuration"
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
</div>
<mat-expansion-panel class="advanced-settings" [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title>
<div fxFlex fxLayout="row" fxLayoutAlign="end center">
<div class="tb-small" translate>device-profile.advanced-settings</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-form-field class="mat-block">
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-expansion-panel>
</div>

View File

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.mat-expansion-panel.advanced-settings {
box-shadow: none;
border: none;
padding: 0;
}
}
:host ::ng-deep {
.mat-expansion-panel.advanced-settings {
.mat-expansion-panel-body {
padding: 0;
}
}
.mat-form-field.duration-value-field {
.mat-form-field-infix {
width: 120px;
}
}
.mat-form-field.duration-unit-field {
.mat-form-field-infix {
width: 120px;
}
}
}

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, NgZone, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -27,11 +27,13 @@ import {
} from '@angular/forms';
import { AlarmRule } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import { TimeUnit, timeUnitTranslationMap } from '../../../../../shared/models/time/time.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({
selector: 'tb-alarm-rule',
templateUrl: './alarm-rule.component.html',
styleUrls: [],
styleUrls: ['./alarm-rule.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -47,9 +49,23 @@ import { MatDialog } from '@angular/material/dialog';
})
export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
timeUnits = Object.keys(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
@Input()
disabled: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
enableDuration = false;
private modelValue: AlarmRule;
alarmRuleFormGroup: FormGroup;
@ -69,7 +85,11 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
ngOnInit() {
this.alarmRuleFormGroup = this.fb.group({
condition: [null, Validators.required],
condition: this.fb.group({
condition: [null, Validators.required],
durationUnit: [null],
durationValue: [null]
}, Validators.required),
alarmDetails: [null]
});
this.alarmRuleFormGroup.valueChanges.subscribe(() => {
@ -88,24 +108,51 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
writeValue(value: AlarmRule): void {
this.modelValue = value;
this.alarmRuleFormGroup.reset(this.modelValue, {emitEvent: false});
this.enableDuration = value && !!value.condition.durationValue;
this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
this.updateValidators();
}
public validate(c: FormControl) {
return (this.alarmRuleFormGroup.valid) ? null : {
return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : {
alarmRule: {
valid: false,
},
};
}
public enableDurationChanged(enableDuration) {
this.enableDuration = enableDuration;
this.updateValidators(true, true);
}
private updateValidators(resetDuration = false, emitEvent = false) {
if (this.enableDuration) {
this.alarmRuleFormGroup.get('condition').get('durationValue')
.setValidators([Validators.required, Validators.min(1), Validators.max(2147483647)]);
this.alarmRuleFormGroup.get('condition').get('durationUnit')
.setValidators([Validators.required]);
} else {
this.alarmRuleFormGroup.get('condition').get('durationValue')
.setValidators([]);
this.alarmRuleFormGroup.get('condition').get('durationUnit')
.setValidators([]);
if (resetDuration) {
this.alarmRuleFormGroup.get('condition').patchValue({
durationValue: null,
durationUnit: null
});
}
}
this.alarmRuleFormGroup.get('condition').get('durationValue').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition').get('durationUnit').updateValueAndValidity({emitEvent});
}
private updateModel() {
if (this.alarmRuleFormGroup.valid) {
const value = this.alarmRuleFormGroup.value;
const value = this.alarmRuleFormGroup.value;
if (this.modelValue) {
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
} else {
this.propagateChange(null);
}
}
}

View File

@ -17,38 +17,42 @@
-->
<div fxFlex fxLayout="column">
<div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" style="padding-left: 20px;" [formGroup]="createAlarmRuleControl">
<mat-form-field class="mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate></mat-label>
<mat-select formControlName="severity"
required
placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="createAlarmRuleControl.get('severity').hasError('required')">
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
</mat-form-field>
<tb-alarm-rule formControlName="alarmRule">
</tb-alarm-rule>
last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity"
required
placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="createAlarmRuleControl.get('severity').hasError('required')">
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
</mat-form-field>
<tb-alarm-rule formControlName="alarmRule" required fxFlex>
</tb-alarm-rule>
</div>
<button *ngIf="!disabled && createAlarmRulesFormArray().controls.length > 1"
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeCreateAlarmRule($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!disabled" fxFlex fxLayout="row" fxLayoutAlign="start center">
<button mat-icon-button color="primary"
<div fxLayout="row" *ngIf="!disabled">
<button mat-stroked-button color="primary"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
<mat-icon>add_circle_outline</mat-icon>
{{ 'device-profile.add-create-alarm-rule' | translate }}
</button>
</div>
</div>

View File

@ -13,6 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
:host {
.create-alarm-rule {
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
padding: 8px;
.mat-form-field.severity {
border-right: 1px groove rgba(0, 0, 0, 0.25);
padding-right: 8px;
}
}
}
:host ::ng-deep {
.mat-form-field.severity {
.mat-form-field-infix {
width: 160px;
}
}
}

View File

@ -150,15 +150,11 @@ export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit,
}
private updateModel() {
if (this.createAlarmRulesFormGroup.valid) {
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {};
value.forEach(v => {
createAlarmRules[v.severity] = v.alarmRule;
});
this.propagateChange(createAlarmRules);
} else {
this.propagateChange(null);
}
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {};
value.forEach(v => {
createAlarmRules[v.severity] = v.alarmRule;
});
this.propagateChange(createAlarmRules);
}
}

View File

@ -1,65 +0,0 @@
<!--
Copyright © 2016-2020 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<form [formGroup]="alarmFormGroup" (ngSubmit)="save()" style="min-width: 600px;">
<mat-toolbar fxLayout="row" color="primary">
<h2>{{ (isReadOnly ? 'device-profile.alarm-rule-details' : (isAdd ? 'device-profile.add-alarm-rule' : 'device-profile.edit-alarm-rule')) | translate }}</h2>
<span fxFlex></span>
<button mat-button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="(isLoading$ | async) || isReadOnly" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>device-profile.alarm-type</mat-label>
<input required matInput formControlName="alarmType">
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')">
{{ 'device-profile.alarm-type-required' | translate }}
</mat-error>
<mat-hint innerHTML="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"></mat-hint>
</mat-form-field>
<fieldset>
<legend translate>device-profile.create-alarm-rules</legend>
</fieldset>
<fieldset>
<legend translate>device-profile.clear-alarm-rule</legend>
</fieldset>
</fieldset>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button *ngIf="!isReadOnly" mat-button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || alarmFormGroup.invalid
|| !alarmFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.save') | translate }}
</button>
<button mat-button color="primary"
style="margin-right: 20px;"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (isReadOnly ? 'action.close' : 'action.cancel') | translate }}
</button>
</div>
</form>

View File

@ -15,33 +15,80 @@
limitations under the License.
-->
<fieldset [formGroup]="alarmFormGroup" class="fields-group tb-device-profile-alarm" fxFlex
style="position: relative;">
<button *ngIf="!disabled" mat-icon-button color="primary" style="min-width: 40px;"
type="button"
style="position: absolute; top: 0; right: 0;"
(click)="removeAlarm.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<legend>
<mat-form-field floatLabel="always"
matTooltip="{{ 'device-profile.alarm-type-pattern-hint' | translate }}">
<mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
<input required matInput formControlName="alarmType" placeholder="Enter alarm type">
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')">
{{ 'device-profile.alarm-type-required' | translate }}
</mat-error>
<!--mat-hint innerHTML="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"></mat-hint-->
</mat-form-field>
</legend>
<mat-expansion-panel class="device-profile-alarm" fxFlex [formGroup]="alarmFormGroup" [(expanded)]="expanded">
<mat-expansion-panel-header>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-panel-title [fxShow]="!expanded">
<div fxLayout="row" fxFlex fxLayoutAlign="start center">
{{ alarmFormGroup.get('alarmType').value }}
</div>
</mat-panel-title>
<mat-form-field floatLabel="always"
style="width: 600px;"
[fxShow]="expanded"
(click)="!disabled ? $event.stopPropagation() : null;">
<mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
<input required matInput formControlName="alarmType" placeholder="Enter alarm type">
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')">
{{ 'device-profile.alarm-type-required' | translate }}
</mat-error>
<mat-hint *ngIf="!disabled"
innerHTML="{{ 'device-profile.alarm-type-pattern-hint' | translate }}"></mat-hint>
</mat-form-field>
<span fxFlex></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeAlarm.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-expansion-panel-header>
<div fxFlex fxLayout="column">
<div translate class="tb-small" style="font-weight: 500;">device-profile.create-alarm-rules</div>
<mat-divider></mat-divider>
<tb-create-alarm-rules formControlName="createRules">
<div translate class="tb-small" style="padding-bottom: 8px;">device-profile.create-alarm-rules</div>
<tb-create-alarm-rules formControlName="createRules" style="padding-bottom: 16px;">
</tb-create-alarm-rules>
<div translate class="tb-small" style="font-weight: 500;">device-profile.clear-alarm-rule</div>
<mat-divider></mat-divider>
<div translate class="tb-small" style="padding-bottom: 8px;">device-profile.clear-alarm-rule</div>
<div fxFlex fxLayout="row"
[fxShow]="alarmFormGroup.get('clearRule').value"
fxLayoutGap="8px;" fxLayoutAlign="start center" style="padding-bottom: 8px;">
<div class="clear-alarm-rule" fxFlex fxLayout="row">
<tb-alarm-rule formControlName="clearRule" fxFlex>
</tb-alarm-rule>
</div>
<button *ngIf="!disabled"
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeClearAlarmRule()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div fxLayout="row" *ngIf="!disabled"
[fxShow]="!alarmFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'device-profile.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon>add_circle_outline</mat-icon>
{{ 'device-profile.add-clear-alarm-rule' | translate }}
</button>
</div>
</div>
</fieldset>
<mat-expansion-panel class="advanced-settings" [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title>
<div fxFlex fxLayout="row" fxLayoutAlign="end center">
<div class="tb-small" translate>device-profile.advanced-settings</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<mat-checkbox formControlName="propagate" style="padding-bottom: 16px;">
{{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox>
<div>TODO: Propagate relation types</div>
</mat-expansion-panel>
</mat-expansion-panel>

View File

@ -13,35 +13,42 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../scss/constants';
:host {
display: block;
.tb-device-profile-alarm {
&.mat-padding {
padding: 8px;
@media #{$mat-gt-sm} {
padding: 16px;
}
}
}
a.mat-icon-button {
&:hover, &:focus {
border-bottom: none;
}
}
.fields-group {
padding: 8px;
margin: 10px 0;
.clear-alarm-rule {
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
padding: 8px;
}
.mat-expansion-panel {
box-shadow: none;
&.device-profile-alarm {
border: 1px groove rgba(0, 0, 0, .25);
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
&.mat-expanded {
height: 80px;
}
}
}
&.advanced-settings {
border: none;
padding: 0;
}
}
}
legend {
padding-left: 8px;
padding-right: 8px;
margin-bottom: -30px;
.mat-form-field {
margin-bottom: 21px;
:host ::ng-deep {
.mat-expansion-panel {
&.device-profile-alarm {
.mat-expansion-panel-body {
padding: 0 8px;
}
}
&.advanced-settings {
.mat-expansion-panel-body {
padding: 0;
}
}
}

View File

@ -19,17 +19,14 @@ import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup, NG_VALIDATORS,
NG_VALUE_ACCESSOR, Validator,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { DeviceProfileAlarm } from '@shared/models/device.models';
import { deepClone } from '@core/utils';
import { AlarmRule, DeviceProfileAlarm } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog';
import {
DeviceProfileAlarmDialogComponent,
DeviceProfileAlarmDialogData
} from './device-profile-alarm-dialog.component';
@Component({
selector: 'tb-device-profile-alarm',
@ -56,6 +53,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
@Output()
removeAlarm = new EventEmitter();
expanded = false;
private modelValue: DeviceProfileAlarm;
alarmFormGroup: FormGroup;
@ -98,31 +97,24 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
writeValue(value: DeviceProfileAlarm): void {
this.modelValue = value;
this.alarmFormGroup.reset(this.modelValue, {emitEvent: false});
if (!this.modelValue.alarmType) {
this.expanded = true;
}
this.alarmFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
}
/* openAlarm($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<DeviceProfileAlarmDialogComponent, DeviceProfileAlarmDialogData,
DeviceProfileAlarm>(DeviceProfileAlarmDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: false,
alarm: this.disabled ? this.modelValue : deepClone(this.modelValue),
isReadOnly: this.disabled
public addClearAlarmRule() {
const clearAlarmRule: AlarmRule = {
condition: {
condition: []
}
}).afterClosed().subscribe(
(deviceProfileAlarm) => {
if (deviceProfileAlarm) {
this.modelValue = deviceProfileAlarm;
this.updateModel();
}
}
);
} */
};
this.alarmFormGroup.patchValue({clearRule: clearAlarmRule});
}
public removeClearAlarmRule() {
this.alarmFormGroup.patchValue({clearRule: null});
}
public validate(c: FormControl) {
return (this.alarmFormGroup.valid) ? null : {
@ -133,12 +125,8 @@ export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit
}
private updateModel() {
if (this.alarmFormGroup.valid) {
const value = this.alarmFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
} else {
this.propagateChange(null);
}
const value = this.alarmFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
}
}

View File

@ -17,8 +17,9 @@
-->
<div fxLayout="column">
<div class="tb-device-profile-alarms">
<div *ngFor="let alarmControl of alarmsFormArray().controls; let $index = index; last as isLast;"
fxLayout="column">
<div *ngFor="let alarmControl of alarmsFormArray().controls; trackBy: trackByAlarm;
let $index = index; last as isLast;"
fxLayout="column" [ngStyle]="!isLast ? {paddingBottom: '8px'} : {}">
<tb-device-profile-alarm [formControl]="alarmControl"
(removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm>

View File

@ -17,7 +17,6 @@
:host {
.tb-device-profile-alarms {
max-height: 400px;
overflow-y: auto;
&.mat-padding {
padding: 8px;

View File

@ -19,9 +19,12 @@ import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder, FormControl,
FormGroup, NG_VALIDATORS,
NG_VALUE_ACCESSOR, Validator,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store';
@ -30,10 +33,6 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileAlarm } from '@shared/models/device.models';
import { guid } from '@core/utils';
import { Subscription } from 'rxjs';
import {
DeviceProfileAlarmDialogComponent,
DeviceProfileAlarmDialogData
} from './device-profile-alarm-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
@ -125,6 +124,14 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
});
}
public trackByAlarm(index: number, alarmControl: AbstractControl): string {
if (alarmControl) {
return alarmControl.value.id;
} else {
return null;
}
}
public removeAlarm(index: number) {
(this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray).removeAt(index);
}
@ -144,22 +151,6 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray;
alarmsArray.push(this.fb.control(alarm, [Validators.required]));
this.deviceProfileAlarmsFormGroup.updateValueAndValidity();
/* this.dialog.open<DeviceProfileAlarmDialogComponent, DeviceProfileAlarmDialogData,
DeviceProfileAlarm>(DeviceProfileAlarmDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: true,
alarm,
isReadOnly: false
}
}).afterClosed().subscribe(
(deviceProfileAlarm) => {
if (deviceProfileAlarm) {
}
}
); */
}
public validate(c: FormControl) {
@ -171,11 +162,11 @@ export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnIni
}
private updateModel() {
if (this.deviceProfileAlarmsFormGroup.valid) {
// if (this.deviceProfileAlarmsFormGroup.valid) {
const alarms: Array<DeviceProfileAlarm> = this.deviceProfileAlarmsFormGroup.get('alarms').value;
this.propagateChange(alarms);
} else {
/* } else {
this.propagateChange(null);
}
} */
}
}

View File

@ -15,7 +15,7 @@
limitations under the License.
-->
<form (ngSubmit)="save()" style="min-width: 600px;">
<form (ngSubmit)="save()" style="min-width: 1000px;">
<mat-toolbar color="primary">
<h2>{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}</h2>
<span fxFlex></span>

View File

@ -52,7 +52,7 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE_PROFILE);
this.config.entityResources = entityTypeResources.get(EntityType.DEVICE_PROFILE);
this.config.addDialogStyle = {width: '600px'};
this.config.addDialogStyle = {width: '1000px'};
this.config.columns.push(
new DateEntityTableColumn<DeviceProfile>('createdTime', 'common.created-time', this.datePipe, '150px'),

View File

@ -148,12 +148,16 @@ export function createDefaultFilterPredicateInfo(valueType: EntityKeyValueType,
const predicate = createDefaultFilterPredicate(valueType, complex);
return {
keyFilterPredicate: predicate,
userInfo: {
editable: true,
label: '',
autogeneratedLabel: true,
order: 0
}
userInfo: createDefaultFilterPredicateUserInfo()
};
}
export function createDefaultFilterPredicateUserInfo(): KeyFilterPredicateUserInfo {
return {
editable: true,
label: '',
autogeneratedLabel: true,
order: 0
};
}
@ -334,6 +338,7 @@ export interface KeyFilterPredicateInfo {
export interface KeyFilter {
key: EntityKey;
valueType: EntityKeyValueType;
predicate: KeyFilterPredicate;
}
@ -353,6 +358,45 @@ export interface FiltersInfo {
datasourceFilters: {[datasourceIndex: number]: FilterInfo};
}
export function keyFilterInfosToKeyFilters(keyFilterInfos: Array<KeyFilterInfo>): Array<KeyFilter> {
const keyFilters: Array<KeyFilter> = [];
for (const keyFilterInfo of keyFilterInfos) {
const key = keyFilterInfo.key;
for (const predicate of keyFilterInfo.predicates) {
const keyFilter: KeyFilter = {
key,
valueType: keyFilterInfo.valueType,
predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate)
};
keyFilters.push(keyFilter);
}
}
return keyFilters;
}
export function keyFiltersToKeyFilterInfos(keyFilters: Array<KeyFilter>): Array<KeyFilterInfo> {
const keyFilterInfos: Array<KeyFilterInfo> = [];
const keyFilterInfoMap: {[infoKey: string]: KeyFilterInfo} = {};
for (const keyFilter of keyFilters) {
const key = keyFilter.key;
const infoKey = key.key + key.type + keyFilter.valueType;
let keyFilterInfo = keyFilterInfoMap[infoKey];
if (!keyFilterInfo) {
keyFilterInfo = {
key,
valueType: keyFilter.valueType,
predicates: []
};
keyFilterInfoMap[infoKey] = keyFilterInfo;
keyFilterInfos.push(keyFilterInfo);
}
if (keyFilter.predicate) {
keyFilterInfo.predicates.push(keyFilterPredicateToKeyFilterPredicateInfo(keyFilter.predicate));
}
}
return keyFilterInfos;
}
export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
const keyFilterInfos = filter.keyFilters;
const keyFilters: Array<KeyFilter> = [];
@ -361,6 +405,7 @@ export function filterInfoToKeyFilters(filter: FilterInfo): Array<KeyFilter> {
for (const predicate of keyFilterInfo.predicates) {
const keyFilter: KeyFilter = {
key,
valueType: keyFilterInfo.valueType,
predicate: keyFilterPredicateInfoToKeyFilterPredicate(predicate)
};
keyFilters.push(keyFilter);
@ -383,6 +428,26 @@ export function keyFilterPredicateInfoToKeyFilterPredicate(keyFilterPredicateInf
return keyFilterPredicate;
}
export function keyFilterPredicateToKeyFilterPredicateInfo(keyFilterPredicate: KeyFilterPredicate): KeyFilterPredicateInfo {
const keyFilterPredicateInfo: KeyFilterPredicateInfo = {
keyFilterPredicate: null,
userInfo: null
};
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
const complexPredicate = keyFilterPredicate as ComplexFilterPredicate;
const predicateInfos = complexPredicate.predicates.map(
predicate => keyFilterPredicateToKeyFilterPredicateInfo(predicate));
keyFilterPredicateInfo.keyFilterPredicate = {
predicates: predicateInfos,
operation: complexPredicate.operation,
type: FilterPredicateType.COMPLEX
} as ComplexFilterPredicateInfo;
} else {
keyFilterPredicateInfo.keyFilterPredicate = keyFilterPredicate;
}
return keyFilterPredicateInfo;
}
export function isFilterEditable(filter: FilterInfo): boolean {
if (filter.editable) {
return filter.keyFilters.some(value => isKeyFilterInfoEditable(value));

View File

@ -815,8 +815,21 @@
"create-alarm-rules": "Create alarm rules",
"clear-alarm-rule": "Clear alarm rule",
"add-create-alarm-rule": "Add create alarm rule",
"add-clear-alarm-rule": "Add clear alarm rule",
"select-alarm-severity": "Select alarm severity",
"alarm-severity-required": "Alarm severity is required."
"alarm-severity-required": "Alarm severity is required.",
"condition-duration": "Condition duration",
"condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit",
"condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.",
"condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm",
"alarm-details": "Alarm details",
"alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Please add alarm rule condition",
"edit-alarm-rule-condition": "Edit alarm rule condition"
},
"dialog": {
"close": "Close dialog"
@ -1286,6 +1299,7 @@
"complex-filter": "Complex filter",
"edit-complex-filter": "Edit complex filter",
"edit-filter-user-params": "Edit filter predicate user parameters",
"filter-user-params": "Filter predicate user parameters",
"user-parameters": "User parameters",
"display-label": "Label to display",
"autogenerated-label": "Auto generate label",

View File

@ -563,7 +563,7 @@ mat-label {
}
}
mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell {
mat-toolbar.mat-table-toolbar:not(.mat-primary), .mat-cell, .mat-expansion-panel-header {
button.mat-icon-button {
mat-icon {
color: rgba(0, 0, 0, .54);