diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java index ed87aea319..f0e51903b5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmCondition.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.common.data.device.profile; +import lombok.Data; import org.thingsboard.server.common.data.query.KeyFilter; import java.util.List; import java.util.concurrent.TimeUnit; +@Data public class AlarmCondition { private List condition; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java index fd50825b88..afdf8abfc5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/AlarmRule.java @@ -15,6 +15,9 @@ */ package org.thingsboard.server.common.data.device.profile; +import lombok.Data; + +@Data public class AlarmRule { private AlarmCondition condition; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java index cbaff61cde..b6437ae7bb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileAlarm.java @@ -33,5 +33,4 @@ public class DeviceProfileAlarm { // Hidden in advanced settings private boolean propagate; private List propagateRelationTypes; - } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 8e779a8106..195e4d3a6e 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -98,6 +98,12 @@ import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.co import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component'; import { MqttDeviceProfileTransportConfigurationComponent } from './profile/device/mqtt-device-profile-transport-configuration.component'; 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'; @NgModule({ declarations: @@ -176,6 +182,12 @@ import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/dev MqttDeviceProfileTransportConfigurationComponent, Lwm2mDeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent, + CreateAlarmRulesComponent, + AlarmRuleComponent, + AlarmRuleConditionComponent, + DeviceProfileAlarmComponent, + DeviceProfileAlarmDialogComponent, + DeviceProfileAlarmsComponent, DeviceProfileDataComponent, DeviceProfileComponent, DeviceProfileDialogComponent @@ -246,6 +258,12 @@ import { Lwm2mDeviceProfileTransportConfigurationComponent } from './profile/dev MqttDeviceProfileTransportConfigurationComponent, Lwm2mDeviceProfileTransportConfigurationComponent, DeviceProfileTransportConfigurationComponent, + CreateAlarmRulesComponent, + AlarmRuleComponent, + AlarmRuleConditionComponent, + DeviceProfileAlarmComponent, + DeviceProfileAlarmDialogComponent, + DeviceProfileAlarmsComponent, DeviceProfileDataComponent, DeviceProfileComponent, DeviceProfileDialogComponent diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html new file mode 100644 index 0000000000..a97a1a1404 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html @@ -0,0 +1,19 @@ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts new file mode 100644 index 0000000000..abfdc4041b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.ts @@ -0,0 +1,112 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmCondition } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-alarm-rule-condition', + templateUrl: './alarm-rule-condition.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleConditionComponent), + multi: true, + } + ] +}) +export class AlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + private modelValue: AlarmCondition; + + alarmRuleConditionFormGroup: FormGroup; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleConditionFormGroup = this.fb.group({ + condition: [null, Validators.required], + durationUnit: [null], + durationValue: [null] + }); + this.alarmRuleConditionFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + 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 { + this.modelValue = value; + this.alarmRuleConditionFormGroup.reset(this.modelValue, {emitEvent: false}); + } + + public validate(c: FormControl) { + return (this.alarmRuleConditionFormGroup.valid) ? null : { + alarmRuleCondition: { + valid: false, + }, + }; + } + + private updateModel() { + if (this.alarmRuleConditionFormGroup.valid) { + const value = this.alarmRuleConditionFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html new file mode 100644 index 0000000000..51569d5311 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html @@ -0,0 +1,22 @@ + +
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts new file mode 100644 index 0000000000..88805de563 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; + +@Component({ + selector: 'tb-alarm-rule', + templateUrl: './alarm-rule.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => AlarmRuleComponent), + multi: true, + } + ] +}) +export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + private modelValue: AlarmRule; + + alarmRuleFormGroup: FormGroup; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmRuleFormGroup = this.fb.group({ + condition: [null, Validators.required], + alarmDetails: [null] + }); + this.alarmRuleFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmRuleFormGroup.disable({emitEvent: false}); + } else { + this.alarmRuleFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AlarmRule): void { + this.modelValue = value; + this.alarmRuleFormGroup.reset(this.modelValue, {emitEvent: false}); + } + + public validate(c: FormControl) { + return (this.alarmRuleFormGroup.valid) ? null : { + alarmRule: { + valid: false, + }, + }; + } + + private updateModel() { + if (this.alarmRuleFormGroup.valid) { + const value = this.alarmRuleFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html new file mode 100644 index 0000000000..9551371af2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html @@ -0,0 +1,54 @@ + +
+
+ + + + + {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} + + + + {{ 'device-profile.alarm-severity-required' | translate }} + + + + + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss new file mode 100644 index 0000000000..bb3718c2da --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.scss @@ -0,0 +1,18 @@ +/** + * 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 { + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts new file mode 100644 index 0000000000..efec9d639c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.ts @@ -0,0 +1,164 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators +} from '@angular/forms'; +import { AlarmRule } from '@shared/models/device.models'; +import { MatDialog } from '@angular/material/dialog'; +import { Subscription } from 'rxjs'; +import { AlarmSeverity, alarmSeverityTranslations } from '../../../../../shared/models/alarm.models'; + +@Component({ + selector: 'tb-create-alarm-rules', + templateUrl: './create-alarm-rules.component.html', + styleUrls: ['./create-alarm-rules.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CreateAlarmRulesComponent), + multi: true, + } + ] +}) +export class CreateAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator { + + alarmSeverities = Object.keys(AlarmSeverity); + alarmSeverityEnum = AlarmSeverity; + + alarmSeverityTranslationMap = alarmSeverityTranslations; + + @Input() + disabled: boolean; + + createAlarmRulesFormGroup: FormGroup; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.createAlarmRulesFormGroup = this.fb.group({ + createAlarmRules: this.fb.array([]) + }); + } + + createAlarmRulesFormArray(): FormArray { + return this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + } + + writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const createAlarmRulesControls: Array = []; + if (createAlarmRules) { + Object.keys(createAlarmRules).forEach((severity) => { + const createAlarmRule = createAlarmRules[severity]; + if (severity === 'empty') { + severity = null; + } + createAlarmRulesControls.push(this.fb.group({ + severity: [severity, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + }); + } + this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls)); + if (this.disabled) { + this.createAlarmRulesFormGroup.disable({emitEvent: false}); + } else { + this.createAlarmRulesFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.createAlarmRulesFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + public removeCreateAlarmRule(index: number) { + (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index); + } + + public addCreateAlarmRule() { + const createAlarmRule: AlarmRule = { + condition: { + condition: [] + } + }; + const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray; + createAlarmRulesArray.push(this.fb.group({ + severity: [null, Validators.required], + alarmRule: [createAlarmRule, Validators.required] + })); + this.createAlarmRulesFormGroup.updateValueAndValidity(); + } + + public validate(c: FormControl) { + return (this.createAlarmRulesFormGroup.valid && this.createAlarmRulesFormGroup.get('createAlarmRules').value.length) ? null : { + createAlarmRules: { + valid: false, + }, + }; + } + + 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); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.html new file mode 100644 index 0000000000..4bc2fe878e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.html @@ -0,0 +1,65 @@ + +
+ +

{{ (isReadOnly ? 'device-profile.alarm-rule-details' : (isAdd ? 'device-profile.add-alarm-rule' : 'device-profile.edit-alarm-rule')) | translate }}

+ + +
+ + +
+
+
+ + device-profile.alarm-type + + + {{ 'device-profile.alarm-type-required' | translate }} + + + +
+ device-profile.create-alarm-rules +
+
+ device-profile.clear-alarm-rule +
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.ts new file mode 100644 index 0000000000..e3a42d7a6e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm-dialog.component.ts @@ -0,0 +1,99 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, + Inject, + 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'; + +export interface DeviceProfileAlarmDialogData { + alarm: DeviceProfileAlarm; + isAdd: boolean; + isReadOnly: boolean; +} + +@Component({ + selector: 'tb-device-profile-alarm-dialog', + templateUrl: './device-profile-alarm-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileAlarmDialogComponent}], + styleUrls: [] +}) +export class DeviceProfileAlarmDialogComponent extends + DialogComponent implements OnInit, ErrorStateMatcher { + + alarmFormGroup: FormGroup; + + isReadOnly = this.data.isReadOnly; + alarm = this.data.alarm; + isAdd = this.data.isAdd; + + submitted = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DeviceProfileAlarmDialogData, + public dialogRef: MatDialogRef, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public fb: FormBuilder) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.alarm = this.data.alarm; + } + + 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 { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.alarmFormGroup.valid) { + this.alarm = {...this.alarm, ...this.alarmFormGroup.value}; + this.dialogRef.close(this.alarm); + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html new file mode 100644 index 0000000000..650982ffdf --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html @@ -0,0 +1,47 @@ + +
+ + + + {{'device-profile.alarm-type' | translate}} + + + {{ 'device-profile.alarm-type-required' | translate }} + + + + +
+
device-profile.create-alarm-rules
+ + + +
device-profile.clear-alarm-rule
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss new file mode 100644 index 0000000000..6e661f9cfc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.scss @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../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; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + padding-left: 8px; + padding-right: 8px; + margin-bottom: -30px; + .mat-form-field { + margin-bottom: 21px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts new file mode 100644 index 0000000000..445ab19a90 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.ts @@ -0,0 +1,144 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormGroup, NG_VALIDATORS, + NG_VALUE_ACCESSOR, Validator, + Validators +} from '@angular/forms'; +import { DeviceProfileAlarm } from '@shared/models/device.models'; +import { deepClone } from '@core/utils'; +import { MatDialog } from '@angular/material/dialog'; +import { + DeviceProfileAlarmDialogComponent, + DeviceProfileAlarmDialogData +} from './device-profile-alarm-dialog.component'; + +@Component({ + selector: 'tb-device-profile-alarm', + templateUrl: './device-profile-alarm.component.html', + styleUrls: ['./device-profile-alarm.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmComponent), + multi: true, + } + ] +}) +export class DeviceProfileAlarmComponent implements ControlValueAccessor, OnInit, Validator { + + @Input() + disabled: boolean; + + @Output() + removeAlarm = new EventEmitter(); + + private modelValue: DeviceProfileAlarm; + + alarmFormGroup: FormGroup; + + private propagateChange = (v: any) => { }; + + constructor(private dialog: MatDialog, + private fb: FormBuilder) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.alarmFormGroup = this.fb.group({ + id: [null, Validators.required], + alarmType: [null, Validators.required], + createRules: [null], + clearRule: [null], + propagate: [null], + propagateRelationTypes: [null] + }); + this.alarmFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.alarmFormGroup.disable({emitEvent: false}); + } else { + this.alarmFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: DeviceProfileAlarm): void { + this.modelValue = value; + this.alarmFormGroup.reset(this.modelValue, {emitEvent: false}); + } + +/* openAlarm($event: Event) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(DeviceProfileAlarmDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: false, + alarm: this.disabled ? this.modelValue : deepClone(this.modelValue), + isReadOnly: this.disabled + } + }).afterClosed().subscribe( + (deviceProfileAlarm) => { + if (deviceProfileAlarm) { + this.modelValue = deviceProfileAlarm; + this.updateModel(); + } + } + ); + } */ + + public validate(c: FormControl) { + return (this.alarmFormGroup.valid) ? null : { + alarm: { + valid: false, + }, + }; + } + + private updateModel() { + if (this.alarmFormGroup.valid) { + const value = this.alarmFormGroup.value; + this.modelValue = {...this.modelValue, ...value}; + this.propagateChange(this.modelValue); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html new file mode 100644 index 0000000000..6d5d0ec865 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html @@ -0,0 +1,38 @@ + +
+
+
+ + +
+
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss new file mode 100644 index 0000000000..22d3556cac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.scss @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import '../scss/constants'; + +:host { + .tb-device-profile-alarms { + max-height: 400px; + overflow-y: auto; + &.mat-padding { + padding: 8px; + @media #{$mat-gt-sm} { + padding: 16px; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts new file mode 100644 index 0000000000..1118bcc163 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.ts @@ -0,0 +1,181 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormArray, + FormBuilder, FormControl, + FormGroup, NG_VALIDATORS, + NG_VALUE_ACCESSOR, Validator, + Validators +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +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({ + selector: 'tb-device-profile-alarms', + templateUrl: './device-profile-alarms.component.html', + styleUrls: ['./device-profile-alarms.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DeviceProfileAlarmsComponent), + multi: true, + } + ] +}) +export class DeviceProfileAlarmsComponent implements ControlValueAccessor, OnInit, Validator { + + deviceProfileAlarmsFormGroup: FormGroup; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + private valueChangeSubscription: Subscription = null; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + private fb: FormBuilder, + private dialog: MatDialog) { + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.deviceProfileAlarmsFormGroup = this.fb.group({ + alarms: this.fb.array([]) + }); + } + + alarmsFormArray(): FormArray { + return this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.deviceProfileAlarmsFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileAlarmsFormGroup.enable({emitEvent: false}); + } + } + + writeValue(alarms: Array | null): void { + if (this.valueChangeSubscription) { + this.valueChangeSubscription.unsubscribe(); + } + const alarmsControls: Array = []; + if (alarms) { + alarms.forEach((alarm) => { + alarmsControls.push(this.fb.control(alarm, [Validators.required])); + }); + } + this.deviceProfileAlarmsFormGroup.setControl('alarms', this.fb.array(alarmsControls)); + if (this.disabled) { + this.deviceProfileAlarmsFormGroup.disable({emitEvent: false}); + } else { + this.deviceProfileAlarmsFormGroup.enable({emitEvent: false}); + } + this.valueChangeSubscription = this.deviceProfileAlarmsFormGroup.valueChanges.subscribe(() => { + this.updateModel(); + }); + } + + public removeAlarm(index: number) { + (this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray).removeAt(index); + } + + public addAlarm() { + const alarm: DeviceProfileAlarm = { + id: guid(), + alarmType: '', + createRules: { + empty: { + condition: { + condition: [] + } + } + } + }; + const alarmsArray = this.deviceProfileAlarmsFormGroup.get('alarms') as FormArray; + alarmsArray.push(this.fb.control(alarm, [Validators.required])); + this.deviceProfileAlarmsFormGroup.updateValueAndValidity(); + +/* this.dialog.open(DeviceProfileAlarmDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd: true, + alarm, + isReadOnly: false + } + }).afterClosed().subscribe( + (deviceProfileAlarm) => { + if (deviceProfileAlarm) { + } + } + ); */ + } + + public validate(c: FormControl) { + return (this.deviceProfileAlarmsFormGroup.valid) ? null : { + alarms: { + valid: false, + }, + }; + } + + private updateModel() { + if (this.deviceProfileAlarmsFormGroup.valid) { + const alarms: Array = this.deviceProfileAlarmsFormGroup.get('alarms').value; + this.propagateChange(alarms); + } else { + this.propagateChange(null); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html index df58cb831c..4b159c5a62 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html @@ -39,5 +39,17 @@ required> + + + +
{{'device-profile.alarm-rules' | translate: + {count: deviceProfileDataFormGroup.get('alarms').value ? + deviceProfileDataFormGroup.get('alarms').value.length : 0} }}
+
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts index 01da29d444..7d7fc55057 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.ts @@ -71,7 +71,8 @@ export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit ngOnInit() { this.deviceProfileDataFormGroup = this.fb.group({ configuration: [null, Validators.required], - transportConfiguration: [null, Validators.required] + transportConfiguration: [null, Validators.required], + alarms: [null] }); this.deviceProfileDataFormGroup.valueChanges.subscribe(() => { this.updateModel(); @@ -96,6 +97,7 @@ export class DeviceProfileDataComponent implements ControlValueAccessor, OnInit deviceTransportTypeConfigurationInfoMap.get(deviceTransportType).hasProfileConfiguration; this.deviceProfileDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); this.deviceProfileDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); + this.deviceProfileDataFormGroup.patchValue({alarms: value?.alarms}, {emitEvent: false}); } private updateModel() { diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 34ff3df061..f4c449ec54 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -23,6 +23,8 @@ import { EntitySearchQuery } from '@shared/models/relation.models'; import { DeviceProfileId } from '@shared/models/id/device-profile-id'; import { RuleChainId } from '@shared/models/id/rule-chain-id'; import { EntityInfoData } from '@shared/models/entity.models'; +import { KeyFilter } from '@shared/models/query/query.models'; +import { TimeUnit } from '@shared/models/time/time.models'; export enum DeviceProfileType { DEFAULT = 'DEFAULT' @@ -198,9 +200,30 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D return transportConfiguration; } +export interface AlarmCondition { + condition: Array; + durationUnit?: TimeUnit; + durationValue?: number; +} + +export interface AlarmRule { + condition: AlarmCondition; + alarmDetails?: string; +} + +export interface DeviceProfileAlarm { + id: string; + alarmType: string; + createRules: {[severity: string]: AlarmRule}; + clearRule?: AlarmRule; + propagate?: boolean; + propagateRelationTypes?: Array; +} + export interface DeviceProfileData { configuration: DeviceProfileConfiguration; transportConfiguration: DeviceProfileTransportConfiguration; + alarms?: Array; } export interface DeviceProfile extends BaseData { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 1437719947..04b27a6865 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -465,3 +465,21 @@ export const defaultTimeIntervals = new Array( value: 30 * DAY } ); + +export enum TimeUnit { + MILLISECONDS = 'MILLISECONDS', + SECONDS = 'SECONDS', + MINUTES = 'MINUTES', + HOURS = 'HOURS', + DAYS = 'DAYS' +} + +export const timeUnitTranslationMap = new Map( + [ + [TimeUnit.MILLISECONDS, 'timeunit.milliseconds'], + [TimeUnit.SECONDS, 'timeunit.seconds'], + [TimeUnit.MINUTES, 'timeunit.minutes'], + [TimeUnit.HOURS, 'timeunit.hours'], + [TimeUnit.DAYS, 'timeunit.days'] + ] +); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 64d697c535..35b632ee05 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -801,7 +801,20 @@ "attributes-topic-filter-required": "Attributes topic filter is required.", "rpc-response-topic-filter": "RPC response topic filter", "rpc-response-topic-filter-required": "RPC response topic filter is required.", - "not-valid-pattern-topic-filter": "Not valid pattern topic filter" + "not-valid-pattern-topic-filter": "Not valid pattern topic filter", + "alarm-rules": "Alarm rules ({{count}})", + "add-alarm-rule": "Add alarm rule", + "edit-alarm-rule": "Edit alarm rule", + "alarm-rule-details": "Alarm rule details", + "alarm-type": "Alarm type", + "alarm-type-required": "Alarm type is required.", + "alarm-type-pattern-hint": "Alarm type pattern, use ${metaKeyName} to substitute variables from metadata", + "create-alarm-pattern": "Create {{alarmType}} alarm", + "create-alarm-rules": "Create alarm rules", + "clear-alarm-rule": "Clear alarm rule", + "add-create-alarm-rule": "Add create alarm rule", + "select-alarm-severity": "Select alarm severity", + "alarm-severity-required": "Alarm severity is required." }, "dialog": { "close": "Close dialog" @@ -1760,6 +1773,13 @@ "seconds": "Seconds", "advanced": "Advanced" }, + "timeunit": { + "milliseconds": "Milliseconds", + "seconds": "Seconds", + "minutes": "Minutes", + "hours": "Hours", + "days": "Days" + }, "timewindow": { "days": "{ days, plural, 1 { day } other {# days } }", "hours": "{ hours, plural, 0 { hour } 1 {1 hour } other {# hours } }",