54 changed files with 1562 additions and 708 deletions
@ -1,63 +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. |
|
||||
*/ |
|
||||
package org.thingsboard.rule.engine.profile; |
|
||||
|
|
||||
import lombok.AccessLevel; |
|
||||
import lombok.Getter; |
|
||||
import org.thingsboard.server.common.data.DeviceProfile; |
|
||||
import org.thingsboard.server.common.data.device.profile.AlarmRule; |
|
||||
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
|
||||
import org.thingsboard.server.common.data.id.DeviceProfileId; |
|
||||
import org.thingsboard.server.common.data.query.EntityKey; |
|
||||
import org.thingsboard.server.common.data.query.KeyFilter; |
|
||||
|
|
||||
import java.util.List; |
|
||||
import java.util.Set; |
|
||||
import java.util.concurrent.ConcurrentHashMap; |
|
||||
import java.util.concurrent.CopyOnWriteArrayList; |
|
||||
|
|
||||
|
|
||||
class DeviceProfileState { |
|
||||
|
|
||||
private DeviceProfile deviceProfile; |
|
||||
@Getter(AccessLevel.PACKAGE) |
|
||||
private final List<DeviceProfileAlarm> alarmSettings = new CopyOnWriteArrayList<>(); |
|
||||
@Getter(AccessLevel.PACKAGE) |
|
||||
private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet(); |
|
||||
|
|
||||
DeviceProfileState(DeviceProfile deviceProfile) { |
|
||||
updateDeviceProfile(deviceProfile); |
|
||||
} |
|
||||
|
|
||||
void updateDeviceProfile(DeviceProfile deviceProfile) { |
|
||||
this.deviceProfile = deviceProfile; |
|
||||
alarmSettings.clear(); |
|
||||
if (deviceProfile.getProfileData().getAlarms() != null) { |
|
||||
alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); |
|
||||
for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { |
|
||||
for (AlarmRule alarmRule : alarm.getCreateRules().values()) { |
|
||||
for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { |
|
||||
entityKeys.add(keyFilter.getKey()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public DeviceProfileId getProfileId() { |
|
||||
return deviceProfile.getId(); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,137 @@ |
|||||
|
/** |
||||
|
* 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.rule.engine.profile; |
||||
|
|
||||
|
import lombok.AccessLevel; |
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.data.DeviceProfile; |
||||
|
import org.thingsboard.server.common.data.alarm.AlarmSeverity; |
||||
|
import org.thingsboard.server.common.data.device.profile.AlarmRule; |
||||
|
import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; |
||||
|
import org.thingsboard.server.common.data.id.DeviceProfileId; |
||||
|
import org.thingsboard.server.common.data.query.ComplexFilterPredicate; |
||||
|
import org.thingsboard.server.common.data.query.DynamicValue; |
||||
|
import org.thingsboard.server.common.data.query.DynamicValueSourceType; |
||||
|
import org.thingsboard.server.common.data.query.EntityKey; |
||||
|
import org.thingsboard.server.common.data.query.EntityKeyType; |
||||
|
import org.thingsboard.server.common.data.query.FilterPredicateValue; |
||||
|
import org.thingsboard.server.common.data.query.KeyFilter; |
||||
|
import org.thingsboard.server.common.data.query.KeyFilterPredicate; |
||||
|
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate; |
||||
|
import org.thingsboard.server.common.data.query.StringFilterPredicate; |
||||
|
|
||||
|
import javax.print.attribute.standard.Severity; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.CopyOnWriteArrayList; |
||||
|
|
||||
|
|
||||
|
class ProfileState { |
||||
|
|
||||
|
private DeviceProfile deviceProfile; |
||||
|
@Getter(AccessLevel.PACKAGE) |
||||
|
private final List<DeviceProfileAlarm> alarmSettings = new CopyOnWriteArrayList<>(); |
||||
|
@Getter(AccessLevel.PACKAGE) |
||||
|
private final Set<EntityKey> entityKeys = ConcurrentHashMap.newKeySet(); |
||||
|
|
||||
|
private final Map<String, Map<AlarmSeverity, Set<EntityKey>>> alarmCreateKeys = new HashMap<>(); |
||||
|
private final Map<String, Set<EntityKey>> alarmClearKeys = new HashMap<>(); |
||||
|
|
||||
|
ProfileState(DeviceProfile deviceProfile) { |
||||
|
updateDeviceProfile(deviceProfile); |
||||
|
} |
||||
|
|
||||
|
void updateDeviceProfile(DeviceProfile deviceProfile) { |
||||
|
this.deviceProfile = deviceProfile; |
||||
|
alarmSettings.clear(); |
||||
|
alarmCreateKeys.clear(); |
||||
|
alarmClearKeys.clear(); |
||||
|
if (deviceProfile.getProfileData().getAlarms() != null) { |
||||
|
alarmSettings.addAll(deviceProfile.getProfileData().getAlarms()); |
||||
|
for (DeviceProfileAlarm alarm : deviceProfile.getProfileData().getAlarms()) { |
||||
|
Map<AlarmSeverity, Set<EntityKey>> createAlarmKeys = alarmCreateKeys.computeIfAbsent(alarm.getId(), id -> new HashMap<>()); |
||||
|
alarm.getCreateRules().forEach(((severity, alarmRule) -> { |
||||
|
Set<EntityKey> ruleKeys = createAlarmKeys.computeIfAbsent(severity, id -> new HashSet<>()); |
||||
|
for (KeyFilter keyFilter : alarmRule.getCondition().getCondition()) { |
||||
|
entityKeys.add(keyFilter.getKey()); |
||||
|
ruleKeys.add(keyFilter.getKey()); |
||||
|
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, ruleKeys); |
||||
|
} |
||||
|
})); |
||||
|
if (alarm.getClearRule() != null) { |
||||
|
Set<EntityKey> clearAlarmKeys = alarmClearKeys.computeIfAbsent(alarm.getId(), id -> new HashSet<>()); |
||||
|
for (KeyFilter keyFilter : alarm.getClearRule().getCondition().getCondition()) { |
||||
|
entityKeys.add(keyFilter.getKey()); |
||||
|
clearAlarmKeys.add(keyFilter.getKey()); |
||||
|
addDynamicValuesRecursively(keyFilter.getPredicate(), entityKeys, clearAlarmKeys); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void addDynamicValuesRecursively(KeyFilterPredicate predicate, Set<EntityKey> entityKeys, Set<EntityKey> ruleKeys) { |
||||
|
switch (predicate.getType()) { |
||||
|
case STRING: |
||||
|
case NUMERIC: |
||||
|
case BOOLEAN: |
||||
|
DynamicValue value = ((SimpleKeyFilterPredicate) predicate).getValue().getDynamicValue(); |
||||
|
if (value != null && value.getSourceType() == DynamicValueSourceType.CURRENT_DEVICE) { |
||||
|
EntityKey entityKey = new EntityKey(EntityKeyType.ATTRIBUTE, value.getSourceAttribute()); |
||||
|
entityKeys.add(entityKey); |
||||
|
ruleKeys.add(entityKey); |
||||
|
} |
||||
|
break; |
||||
|
case COMPLEX: |
||||
|
for (KeyFilterPredicate child : ((ComplexFilterPredicate) predicate).getPredicates()) { |
||||
|
addDynamicValuesRecursively(child, entityKeys, ruleKeys); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
DeviceProfileId getProfileId() { |
||||
|
return deviceProfile.getId(); |
||||
|
} |
||||
|
|
||||
|
Set<EntityKey> getCreateAlarmKeys(String id, AlarmSeverity severity) { |
||||
|
Map<AlarmSeverity, Set<EntityKey>> sKeys = alarmCreateKeys.get(id); |
||||
|
if (sKeys == null) { |
||||
|
return Collections.emptySet(); |
||||
|
} else { |
||||
|
Set<EntityKey> keys = sKeys.get(severity); |
||||
|
if (keys == null) { |
||||
|
return Collections.emptySet(); |
||||
|
} else { |
||||
|
return keys; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
Set<EntityKey> getClearAlarmKeys(String id) { |
||||
|
Set<EntityKey> keys = alarmClearKeys.get(id); |
||||
|
if (keys == null) { |
||||
|
return Collections.emptySet(); |
||||
|
} else { |
||||
|
return keys; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
/** |
||||
|
* 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.rule.engine.profile; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.data.query.EntityKey; |
||||
|
import org.thingsboard.server.common.data.query.EntityKeyType; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
|
||||
|
class SnapshotUpdate { |
||||
|
|
||||
|
@Getter |
||||
|
private final EntityKeyType type; |
||||
|
@Getter |
||||
|
private final Set<EntityKey> keys; |
||||
|
|
||||
|
SnapshotUpdate(EntityKeyType type, Set<EntityKey> keys) { |
||||
|
this.type = type; |
||||
|
this.keys = keys; |
||||
|
} |
||||
|
|
||||
|
boolean hasUpdate(){ |
||||
|
return !keys.isEmpty(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,124 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="conditionFormGroup" (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" |
||||
|
[allowUserDynamicSource]="false" |
||||
|
[telemetryKeysOnly]="true" |
||||
|
formControlName="keyFilters"> |
||||
|
</tb-key-filter-list> |
||||
|
<section formGroupName="spec" class="row"> |
||||
|
<mat-form-field class="mat-block" hideRequiredMarker> |
||||
|
<mat-label translate>device-profile.condition-type</mat-label> |
||||
|
<mat-select formControlName="type" required> |
||||
|
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> |
||||
|
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')"> |
||||
|
{{ 'device-profile.condition-type-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('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="conditionFormGroup.get('spec.value').hasError('required')"> |
||||
|
{{ 'device-profile.condition-duration-value-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('min')"> |
||||
|
{{ 'device-profile.condition-duration-value-range' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('spec.value').hasError('max')"> |
||||
|
{{ 'device-profile.condition-duration-value-range' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('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="conditionFormGroup.get('spec.unit').hasError('required')"> |
||||
|
{{ 'device-profile.condition-duration-time-unit-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div fxLayout="row" fxLayoutGap="8px" *ngIf="conditionFormGroup.get('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="conditionFormGroup.get('spec.count').hasError('required')"> |
||||
|
{{ 'device-profile.condition-repeating-value-required' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('min')"> |
||||
|
{{ 'device-profile.condition-repeating-value-range' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('max')"> |
||||
|
{{ 'device-profile.condition-repeating-value-range' | translate }} |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="conditionFormGroup.get('spec.count').hasError('pattern')"> |
||||
|
{{ 'device-profile.condition-repeating-value-pattern' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
*ngIf="!readonly" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.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> |
||||
@ -0,0 +1,150 @@ |
|||||
|
///
|
||||
|
/// 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 { Router } from '@angular/router'; |
||||
|
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'; |
||||
|
import { AlarmCondition, AlarmConditionType, AlarmConditionTypeTranslationMap } from '@shared/models/device.models'; |
||||
|
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; |
||||
|
|
||||
|
export interface AlarmRuleConditionDialogData { |
||||
|
readonly: boolean; |
||||
|
condition: AlarmCondition; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-condition-dialog', |
||||
|
templateUrl: './alarm-rule-condition-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleConditionDialogComponent}], |
||||
|
styleUrls: ['/alarm-rule-condition-dialog.component.scss'] |
||||
|
}) |
||||
|
export class AlarmRuleConditionDialogComponent extends DialogComponent<AlarmRuleConditionDialogComponent, AlarmCondition> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
timeUnits = Object.keys(TimeUnit); |
||||
|
timeUnitTranslations = timeUnitTranslationMap; |
||||
|
alarmConditionTypes = Object.keys(AlarmConditionType); |
||||
|
AlarmConditionType = AlarmConditionType; |
||||
|
alarmConditionTypeTranslation = AlarmConditionTypeTranslationMap; |
||||
|
|
||||
|
readonly = this.data.readonly; |
||||
|
condition = this.data.condition; |
||||
|
|
||||
|
conditionFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleConditionDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<AlarmRuleConditionDialogComponent, AlarmCondition>, |
||||
|
private fb: FormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
public translate: TranslateService) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.conditionFormGroup = this.fb.group({ |
||||
|
keyFilters: [keyFiltersToKeyFilterInfos(this.condition?.condition), Validators.required], |
||||
|
spec: this.fb.group({ |
||||
|
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]*')]] |
||||
|
}) |
||||
|
}); |
||||
|
this.conditionFormGroup.patchValue({spec: this.condition?.spec}); |
||||
|
this.conditionFormGroup.get('spec.type').valueChanges.subscribe((type) => { |
||||
|
this.updateValidators(type, true, true); |
||||
|
}); |
||||
|
if (this.readonly) { |
||||
|
this.conditionFormGroup.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.updateValidators(this.condition?.spec?.type); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
private updateValidators(type: AlarmConditionType, resetDuration = false, emitEvent = false) { |
||||
|
switch (type) { |
||||
|
case AlarmConditionType.DURATION: |
||||
|
this.conditionFormGroup.get('spec.value').enable(); |
||||
|
this.conditionFormGroup.get('spec.unit').enable(); |
||||
|
this.conditionFormGroup.get('spec.count').disable(); |
||||
|
if (resetDuration) { |
||||
|
this.conditionFormGroup.get('spec').patchValue({ |
||||
|
count: null |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
case AlarmConditionType.REPEATING: |
||||
|
this.conditionFormGroup.get('spec.count').enable(); |
||||
|
this.conditionFormGroup.get('spec.value').disable(); |
||||
|
this.conditionFormGroup.get('spec.unit').disable(); |
||||
|
if (resetDuration) { |
||||
|
this.conditionFormGroup.get('spec').patchValue({ |
||||
|
value: null, |
||||
|
unit: null |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
case AlarmConditionType.SIMPLE: |
||||
|
this.conditionFormGroup.get('spec.value').disable(); |
||||
|
this.conditionFormGroup.get('spec.unit').disable(); |
||||
|
this.conditionFormGroup.get('spec.count').disable(); |
||||
|
if (resetDuration) { |
||||
|
this.conditionFormGroup.get('spec').patchValue({ |
||||
|
value: null, |
||||
|
unit: null, |
||||
|
count: null |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
this.conditionFormGroup.get('spec.value').updateValueAndValidity({emitEvent}); |
||||
|
this.conditionFormGroup.get('spec.unit').updateValueAndValidity({emitEvent}); |
||||
|
this.conditionFormGroup.get('spec.count').updateValueAndValidity({emitEvent}); |
||||
|
} |
||||
|
|
||||
|
cancel(): void { |
||||
|
this.dialogRef.close(null); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
this.submitted = true; |
||||
|
this.condition = { |
||||
|
condition: keyFilterInfosToKeyFilters(this.conditionFormGroup.get('keyFilters').value), |
||||
|
spec: this.conditionFormGroup.get('spec').value |
||||
|
}; |
||||
|
this.dialogRef.close(this.condition); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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. |
||||
|
|
||||
|
--> |
||||
|
<div fxLayout="column" fxFlex> |
||||
|
<div fxLayout="row" fxLayoutAlign="start center" style="min-height: 40px;"> |
||||
|
<label class="tb-title" translate>device-profile.schedule</label> |
||||
|
<span fxFlex></span> |
||||
|
<a mat-button color="primary" |
||||
|
type="button" |
||||
|
(click)="openScheduleDialog($event)" |
||||
|
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ (disabled ? 'action.view' : 'action.edit' ) | translate }} |
||||
|
</a> |
||||
|
</div> |
||||
|
<sapn class="tb-alarm-rule-schedule" [ngClass]="{disabled: this.disabled}" (click)="openScheduleDialog($event)" |
||||
|
[innerHTML]="scheduleText"> |
||||
|
</sapn> |
||||
|
</div> |
||||
@ -0,0 +1,34 @@ |
|||||
|
/** |
||||
|
* 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 { |
||||
|
display: flex; |
||||
|
a.mat-button { |
||||
|
&:hover, &:focus { |
||||
|
border-bottom: none; |
||||
|
} |
||||
|
} |
||||
|
.tb-alarm-rule-schedule { |
||||
|
line-height: 1.8em; |
||||
|
padding: 4px; |
||||
|
cursor: pointer; |
||||
|
&.disabled { |
||||
|
opacity: 0.7; |
||||
|
} |
||||
|
.nowrap { |
||||
|
white-space: nowrap; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,137 @@ |
|||||
|
///
|
||||
|
/// 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 { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { |
||||
|
AlarmSchedule, |
||||
|
AlarmScheduleType, |
||||
|
AlarmScheduleTypeTranslationMap, dayOfWeekTranslations, |
||||
|
getAlarmScheduleRangeText, utcTimestampToTimeOfDay |
||||
|
} from '@shared/models/device.models'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { |
||||
|
AlarmScheduleDialogComponent, |
||||
|
AlarmScheduleDialogData |
||||
|
} from '@home/components/profile/alarm/alarm-schedule-dialog.component'; |
||||
|
import { deepClone, isDefinedAndNotNull } from '@core/utils'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-schedule-info', |
||||
|
templateUrl: './alarm-schedule-info.component.html', |
||||
|
styleUrls: ['./alarm-schedule-info.component.scss'], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmScheduleInfoComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
export class AlarmScheduleInfoComponent implements ControlValueAccessor, OnInit { |
||||
|
|
||||
|
@Input() |
||||
|
disabled: boolean; |
||||
|
|
||||
|
private modelValue: AlarmSchedule; |
||||
|
|
||||
|
scheduleText = ''; |
||||
|
|
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
constructor(private dialog: MatDialog, |
||||
|
private translate: TranslateService, |
||||
|
private cd: ChangeDetectorRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
this.disabled = isDisabled; |
||||
|
} |
||||
|
|
||||
|
writeValue(value: AlarmSchedule): void { |
||||
|
this.modelValue = value; |
||||
|
this.updateScheduleText(); |
||||
|
} |
||||
|
|
||||
|
private updateScheduleText() { |
||||
|
let schedule = this.modelValue; |
||||
|
if (!isDefinedAndNotNull(schedule)) { |
||||
|
schedule = { |
||||
|
type: AlarmScheduleType.ANY_TIME |
||||
|
}; |
||||
|
} |
||||
|
this.scheduleText = ''; |
||||
|
switch (schedule.type) { |
||||
|
case AlarmScheduleType.ANY_TIME: |
||||
|
this.scheduleText = this.translate.instant('device-profile.schedule-any-time'); |
||||
|
break; |
||||
|
case AlarmScheduleType.SPECIFIC_TIME: |
||||
|
for (const day of schedule.daysOfWeek) { |
||||
|
if (this.scheduleText.length) { |
||||
|
this.scheduleText += ', '; |
||||
|
} |
||||
|
this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]); |
||||
|
} |
||||
|
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.startsOn), |
||||
|
utcTimestampToTimeOfDay(schedule.endsOn)) + '</b>'; |
||||
|
break; |
||||
|
case AlarmScheduleType.CUSTOM: |
||||
|
for (const item of schedule.items) { |
||||
|
if (item.enabled) { |
||||
|
if (this.scheduleText.length) { |
||||
|
this.scheduleText += '<br/>'; |
||||
|
} |
||||
|
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]); |
||||
|
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn), |
||||
|
utcTimestampToTimeOfDay(item.endsOn)) + '</b>'; |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public openScheduleDialog($event: Event) { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
} |
||||
|
this.dialog.open<AlarmScheduleDialogComponent, AlarmScheduleDialogData, |
||||
|
AlarmSchedule>(AlarmScheduleDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
readonly: this.disabled, |
||||
|
alarmSchedule: this.disabled ? this.modelValue : deepClone(this.modelValue) |
||||
|
} |
||||
|
}).afterClosed().subscribe((result) => { |
||||
|
if (result) { |
||||
|
this.modelValue = result; |
||||
|
this.propagateChange(this.modelValue); |
||||
|
this.updateScheduleText(); |
||||
|
this.cd.detectChanges(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
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]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px;"> |
||||
|
<mat-toolbar color="primary"> |
||||
|
<h2>{{ 'device-profile.alarm-rule-details' | 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"> |
||||
|
<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> |
||||
|
</div> |
||||
|
</fieldset> |
||||
|
</div> |
||||
|
<div mat-dialog-actions fxLayoutAlign="end center"> |
||||
|
<button mat-raised-button color="primary" |
||||
|
type="submit" |
||||
|
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty"> |
||||
|
{{ 'action.save' | translate }} |
||||
|
</button> |
||||
|
<button mat-button color="primary" |
||||
|
type="button" |
||||
|
[disabled]="(isLoading$ | async)" |
||||
|
(click)="cancel()" cdkFocusInitial> |
||||
|
{{ 'action.cancel' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</form> |
||||
@ -0,0 +1,80 @@ |
|||||
|
///
|
||||
|
/// 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 } from '@angular/forms'; |
||||
|
import { Router } from '@angular/router'; |
||||
|
import { DialogComponent } from '@app/shared/components/dialog.component'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
|
||||
|
export interface EditAlarmDetailsDialogData { |
||||
|
alarmDetails: string; |
||||
|
} |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-edit-alarm-details-dialog', |
||||
|
templateUrl: './edit-alarm-details-dialog.component.html', |
||||
|
providers: [{provide: ErrorStateMatcher, useExisting: EditAlarmDetailsDialogComponent}], |
||||
|
styleUrls: [] |
||||
|
}) |
||||
|
export class EditAlarmDetailsDialogComponent extends DialogComponent<EditAlarmDetailsDialogComponent, string> |
||||
|
implements OnInit, ErrorStateMatcher { |
||||
|
|
||||
|
alarmDetails = this.data.alarmDetails; |
||||
|
|
||||
|
editDetailsFormGroup: FormGroup; |
||||
|
|
||||
|
submitted = false; |
||||
|
|
||||
|
constructor(protected store: Store<AppState>, |
||||
|
protected router: Router, |
||||
|
@Inject(MAT_DIALOG_DATA) public data: EditAlarmDetailsDialogData, |
||||
|
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, |
||||
|
public dialogRef: MatDialogRef<EditAlarmDetailsDialogComponent, string>, |
||||
|
private fb: FormBuilder, |
||||
|
private utils: UtilsService, |
||||
|
public translate: TranslateService) { |
||||
|
super(store, router, dialogRef); |
||||
|
|
||||
|
this.editDetailsFormGroup = this.fb.group({ |
||||
|
alarmDetails: [this.alarmDetails] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value; |
||||
|
this.dialogRef.close(this.alarmDetails); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue