UI: Added device profile alarm conditional type

This commit is contained in:
Vladyslav_Prykhodko 2020-10-05 18:18:46 +03:00
parent 1278339e61
commit 77d2c786af
11 changed files with 184 additions and 130 deletions

View File

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.common.data.device.profile; package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data; import lombok.Data;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DurationAlarmConditionSpec implements AlarmConditionSpec { public class DurationAlarmConditionSpec implements AlarmConditionSpec {
private TimeUnit unit; private TimeUnit unit;

View File

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.common.data.device.profile; package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data; import lombok.Data;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class RepeatingAlarmConditionSpec implements AlarmConditionSpec { public class RepeatingAlarmConditionSpec implements AlarmConditionSpec {
private int count; private int count;

View File

@ -15,9 +15,11 @@
*/ */
package org.thingsboard.server.common.data.device.profile; package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data; import lombok.Data;
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SimpleAlarmConditionSpec implements AlarmConditionSpec { public class SimpleAlarmConditionSpec implements AlarmConditionSpec {
@Override @Override
public AlarmConditionSpecType getType() { public AlarmConditionSpecType getType() {

View File

@ -17,7 +17,6 @@
--> -->
<div fxLayout="column" fxFlex> <div fxLayout="column" fxFlex>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> <div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;">
<div class="tb-small" translate>device-profile.alarm-rule-condition</div>
<span fxFlex></span> <span fxFlex></span>
<a mat-button color="primary" <a mat-button color="primary"
type="button" type="button"

View File

@ -16,70 +16,90 @@
--> -->
<div fxLayout="column" [formGroup]="alarmRuleFormGroup"> <div fxLayout="column" [formGroup]="alarmRuleFormGroup">
<div formGroupName="condition" fxLayout="row" fxLayoutGap="8px" fxFlex> <mat-tab-group>
<tb-alarm-rule-condition fxFlex <mat-tab label="{{ 'device-profile.condition' | translate }}" formGroupName="condition">
formControlName="condition"> <tb-alarm-rule-condition fxFlex class="row"
</tb-alarm-rule-condition> formControlName="condition">
<div fxLayout="column"> </tb-alarm-rule-condition>
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> <section class="row">
<div class="tb-small" translate>device-profile.condition-duration</div> <div formGroupName="spec">
<span fxFlex></span> <mat-form-field class="mat-block" hideRequiredMarker>
<mat-slide-toggle [disabled]="disabled" <mat-label translate>device-profile.condition-type</mat-label>
color="primary" <mat-select formControlName="type" required>
[ngModelOptions]="{standalone: true}" <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
(ngModelChange)="enableDurationChanged($event)" {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
[ngModel]="enableDuration">
</mat-slide-toggle>
</div>
<div class="tb-condition-duration" fxFlex fxLayout="row" fxLayoutGap="8px">
<span style="min-width: 250px;" *ngIf="!enableDuration"></span>
<div style="min-width: 250px;" fxLayout="row" fxLayoutGap="8px" *ngIf="enableDuration">
<mat-form-field class="mat-block duration-value-field" hideRequiredMarker floatLabel="always">
<mat-label></mat-label>
<input type="number"
required
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
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option> </mat-option>
</mat-select> </mat-select>
<mat-error *ngIf="alarmRuleFormGroup.get('condition').get('durationUnit').hasError('required')"> <mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.type').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }} {{ 'device-profile.condition-type-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.DURATION">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-duration-value' | translate }}"
formControlName="value">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('required')">
{{ 'device-profile.condition-duration-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('min')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('max')">
{{ 'device-profile.condition-duration-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.value').hasError('pattern')">
{{ 'device-profile.condition-duration-value-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<mat-select formControlName="unit"
required
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.spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="8px" *ngIf="alarmRuleFormGroup.get('condition.spec.type').value == AlarmConditionType.REPEATING">
<mat-form-field class="mat-block" hideRequiredMarker fxFlex floatLabel="always">
<mat-label></mat-label>
<input type="number" required
step="1" min="1" max="2147483647" matInput
placeholder="{{ 'device-profile.condition-repeating-value' | translate }}"
formControlName="count">
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('required')">
{{ 'device-profile.condition-repeating-value-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('min')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('max')">
{{ 'device-profile.condition-repeating-value-range' | translate }}
</mat-error>
<mat-error *ngIf="alarmRuleFormGroup.get('condition.spec.count').hasError('pattern')">
{{ 'device-profile.condition-repeating-value-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</div> </div>
</div> </section>
</div> </mat-tab>
</div> <mat-tab label="{{ 'device-profile.schedule' | translate }}">
<mat-expansion-panel class="advanced-settings" [expanded]="false"> <div class="row">{{ 'device-profile.schedule' | translate }}</div>
<mat-expansion-panel-header> </mat-tab>
<mat-panel-title> <mat-tab label="{{ 'device-profile.alarm-rule-details' | translate }}">
<div fxFlex fxLayout="row" fxLayoutAlign="end center"> <mat-form-field class="mat-block row">
<div class="tb-small" translate>device-profile.alarm-rule-details</div> <mat-label translate>device-profile.alarm-details</mat-label>
</div> <textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-panel-title> </mat-form-field>
</mat-expansion-panel-header> </mat-tab>
<mat-form-field class="mat-block"> </mat-tab-group>
<mat-label translate>device-profile.alarm-details</mat-label>
<textarea matInput formControlName="alarmDetails" rows="5"></textarea>
</mat-form-field>
</mat-expansion-panel>
</div> </div>

View File

@ -14,33 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-condition-duration { .row {
padding: 8px; margin-top: 1em;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
}
.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. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, forwardRef, Input, NgZone, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -25,9 +25,9 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { AlarmRule } from '@shared/models/device.models'; import { AlarmConditionType, AlarmConditionTypeTranslationMap, AlarmRule } from '@shared/models/device.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { TimeUnit, timeUnitTranslationMap } from '../../../../../shared/models/time/time.models'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
@Component({ @Component({
@ -51,6 +51,9 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
timeUnits = Object.keys(TimeUnit); timeUnits = Object.keys(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap; timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.keys(AlarmConditionType);
AlarmConditionType = AlarmConditionType;
alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -64,8 +67,6 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.requiredValue = coerceBooleanProperty(value); this.requiredValue = coerceBooleanProperty(value);
} }
enableDuration = false;
private modelValue: AlarmRule; private modelValue: AlarmRule;
alarmRuleFormGroup: FormGroup; alarmRuleFormGroup: FormGroup;
@ -87,11 +88,18 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
this.alarmRuleFormGroup = this.fb.group({ this.alarmRuleFormGroup = this.fb.group({
condition: this.fb.group({ condition: this.fb.group({
condition: [null, Validators.required], condition: [null, Validators.required],
durationUnit: [null], spec: this.fb.group({
durationValue: [null] type: [AlarmConditionType.SIMPLE, Validators.required],
unit: [{value: null, disable: true}, Validators.required],
value: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
count: [{value: null, disable: true}, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]]
})
}, Validators.required), }, Validators.required),
alarmDetails: [null] alarmDetails: [null]
}); });
this.alarmRuleFormGroup.get('condition.spec.type').valueChanges.subscribe((type) => {
this.updateValidators(type, true, true);
});
this.alarmRuleFormGroup.valueChanges.subscribe(() => { this.alarmRuleFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });
@ -108,9 +116,13 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
writeValue(value: AlarmRule): void { writeValue(value: AlarmRule): void {
this.modelValue = value; this.modelValue = value;
this.enableDuration = value && !!value.condition.durationValue; if (this.modelValue?.condition?.spec === null) {
this.modelValue.condition.spec = {
type: AlarmConditionType.SIMPLE
};
}
this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false}); this.alarmRuleFormGroup.reset(this.modelValue || undefined, {emitEvent: false});
this.updateValidators(); this.updateValidators(this.modelValue?.condition?.spec?.type);
} }
public validate(c: FormControl) { public validate(c: FormControl) {
@ -121,31 +133,45 @@ export class AlarmRuleComponent implements ControlValueAccessor, OnInit, Validat
}; };
} }
public enableDurationChanged(enableDuration) { private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) {
this.enableDuration = enableDuration; switch (type) {
this.updateValidators(true, true); case AlarmConditionType.DURATION:
} this.alarmRuleFormGroup.get('condition.spec.value').enable();
this.alarmRuleFormGroup.get('condition.spec.unit').enable();
private updateValidators(resetDuration = false, emitEvent = false) { this.alarmRuleFormGroup.get('condition.spec.count').disable();
if (this.enableDuration) { if (resetDuration) {
this.alarmRuleFormGroup.get('condition').get('durationValue') this.alarmRuleFormGroup.get('condition.spec').patchValue({
.setValidators([Validators.required, Validators.min(1), Validators.max(2147483647)]); count: null
this.alarmRuleFormGroup.get('condition').get('durationUnit') });
.setValidators([Validators.required]); }
} else { break;
this.alarmRuleFormGroup.get('condition').get('durationValue') case AlarmConditionType.REPEATING:
.setValidators([]); this.alarmRuleFormGroup.get('condition.spec.count').enable();
this.alarmRuleFormGroup.get('condition').get('durationUnit') this.alarmRuleFormGroup.get('condition.spec.value').disable();
.setValidators([]); this.alarmRuleFormGroup.get('condition.spec.unit').disable();
if (resetDuration) { if (resetDuration) {
this.alarmRuleFormGroup.get('condition').patchValue({ this.alarmRuleFormGroup.get('condition.spec').patchValue({
durationValue: null, value: null,
durationUnit: null unit: null
}); });
} }
break;
case AlarmConditionType.SIMPLE:
this.alarmRuleFormGroup.get('condition.spec.value').disable();
this.alarmRuleFormGroup.get('condition.spec.unit').disable();
this.alarmRuleFormGroup.get('condition.spec.count').disable();
if (resetDuration) {
this.alarmRuleFormGroup.get('condition.spec').patchValue({
value: null,
unit: null,
count: null
});
}
break;
} }
this.alarmRuleFormGroup.get('condition').get('durationValue').updateValueAndValidity({emitEvent}); this.alarmRuleFormGroup.get('condition.spec.value').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition').get('durationUnit').updateValueAndValidity({emitEvent}); this.alarmRuleFormGroup.get('condition.spec.unit').updateValueAndValidity({emitEvent});
this.alarmRuleFormGroup.get('condition.spec.count').updateValueAndValidity({emitEvent});
} }
private updateModel() { private updateModel() {

View File

@ -19,7 +19,7 @@
<div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index; <div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
last as isLast;" fxLayout="row" fxLayoutAlign="start center" last as isLast;" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl"> fxLayoutGap="8px" style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start"> <div class="create-alarm-rule" fxFlex fxLayout="column" fxLayoutGap="8px" fxLayoutAlign="start">
<mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker> <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>alarm.severity</mat-label> <mat-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity" <mat-select formControlName="severity"

View File

@ -19,10 +19,6 @@
border: 1px groove rgba(0, 0, 0, .25); border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px; border-radius: 4px;
padding: 8px; padding: 8px;
.mat-form-field.severity {
border-right: 1px groove rgba(0, 0, 0, 0.25);
padding-right: 8px;
}
} }
} }

View File

@ -210,10 +210,30 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D
return transportConfiguration; return transportConfiguration;
} }
export enum AlarmConditionType {
SIMPLE = 'SIMPLE',
DURATION = 'DURATION',
REPEATING = 'REPEATING'
}
export const AlarmConditionTypeTranslationMap = new Map<AlarmConditionType, string>(
[
[AlarmConditionType.SIMPLE, 'device-profile.condition-type-simple'],
[AlarmConditionType.DURATION, 'device-profile.condition-type-duration'],
[AlarmConditionType.REPEATING, 'device-profile.condition-type-repeating']
]
);
export interface AlarmConditionSpec{
type?: AlarmConditionType;
unit?: TimeUnit;
value?: number;
count?: number;
}
export interface AlarmCondition { export interface AlarmCondition {
condition: Array<KeyFilter>; condition: Array<KeyFilter>;
durationUnit?: TimeUnit; spec?: AlarmConditionSpec;
durationValue?: number;
} }
export interface AlarmRule { export interface AlarmRule {

View File

@ -834,6 +834,7 @@
"condition-duration-value": "Duration value", "condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit", "condition-duration-time-unit": "Time unit",
"condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.", "condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.",
"condition-duration-value-pattern": "Duration value should be integers.",
"condition-duration-value-required": "Duration value is required.", "condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.", "condition-duration-time-unit-required": "Time unit is required.",
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
@ -844,7 +845,18 @@
"alarm-details": "Alarm details", "alarm-details": "Alarm details",
"alarm-rule-condition": "Alarm rule condition", "alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Please add alarm rule condition", "enter-alarm-rule-condition-prompt": "Please add alarm rule condition",
"edit-alarm-rule-condition": "Edit alarm rule condition" "edit-alarm-rule-condition": "Edit alarm rule condition",
"condition": "Condition",
"condition-type": "Condition type",
"condition-type-simple": "Simple",
"condition-type-duration": "Duration",
"condition-type-repeating": "Repeating",
"condition-type-required": "Condition type is required.",
"condition-repeating-value": "Count of events",
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"schedule": "Schedule"
}, },
"dialog": { "dialog": {
"close": "Close dialog" "close": "Close dialog"