committed by
GitHub
122 changed files with 2274 additions and 910 deletions
@ -0,0 +1,43 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.alarm.rule.condition.expression.predicate; |
||||
|
|
||||
|
import jakarta.validation.Valid; |
||||
|
import jakarta.validation.constraints.NotNull; |
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
import org.thingsboard.server.common.data.alarm.rule.condition.AlarmConditionValue; |
||||
|
|
||||
|
import java.util.concurrent.TimeUnit; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
public class NoDataFilterPredicate implements KeyFilterPredicate { |
||||
|
|
||||
|
@NotNull |
||||
|
private TimeUnit unit; |
||||
|
@Valid |
||||
|
@NotNull |
||||
|
private AlarmConditionValue<Long> duration; |
||||
|
|
||||
|
@Override |
||||
|
public FilterPredicateType getType() { |
||||
|
return FilterPredicateType.NO_DATA; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2025 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.common.data.query; |
||||
|
|
||||
|
import io.swagger.v3.oas.annotations.media.ArraySchema; |
||||
|
import io.swagger.v3.oas.annotations.media.Schema; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
|
||||
|
import static java.util.Collections.emptyList; |
||||
|
import static java.util.Collections.emptySet; |
||||
|
import static java.util.Objects.requireNonNullElse; |
||||
|
|
||||
|
@Schema( |
||||
|
description = "Contains unique time series and attribute key names discovered from entities matching a query. Used primarily for UI hints such as autocomplete suggestions." |
||||
|
) |
||||
|
public record AvailableEntityKeys( |
||||
|
@Schema( |
||||
|
description = "Set of entity types found among the matched entities.", |
||||
|
example = "[\"DEVICE\", \"ASSET\"]", |
||||
|
requiredMode = Schema.RequiredMode.REQUIRED |
||||
|
) |
||||
|
Set<EntityType> entityTypes, |
||||
|
|
||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) |
||||
|
@ArraySchema( |
||||
|
arraySchema = @Schema(description = "List of unique time series key names available on the matched entities."), |
||||
|
schema = @Schema(implementation = String.class, example = "temperature"), |
||||
|
uniqueItems = true |
||||
|
) |
||||
|
List<String> timeseries, |
||||
|
|
||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) |
||||
|
@ArraySchema( |
||||
|
arraySchema = @Schema(description = "List of unique attribute key names available on the matched entities."), |
||||
|
schema = @Schema(implementation = String.class, example = "serialNumber"), |
||||
|
uniqueItems = true |
||||
|
) |
||||
|
List<String> attribute |
||||
|
) { |
||||
|
|
||||
|
public AvailableEntityKeys { |
||||
|
entityTypes = requireNonNullElse(entityTypes, emptySet()); |
||||
|
timeseries = requireNonNullElse(timeseries, emptyList()); |
||||
|
attribute = requireNonNullElse(attribute, emptyList()); |
||||
|
} |
||||
|
|
||||
|
public static AvailableEntityKeys none() { |
||||
|
return new AvailableEntityKeys(emptySet(), emptyList(), emptyList()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2025 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 class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueNoDataFormGroup"> |
||||
|
<div class="tb-form-row no-padding no-border column-xs"> |
||||
|
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}"> |
||||
|
<mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option> |
||||
|
<mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
<ng-container formGroupName="duration"> |
||||
|
@if (!dynamicModeControl.value) { |
||||
|
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<input required type="number" matInput formControlName="staticValue" min="1" |
||||
|
placeholder="{{'filter.value' | translate}}"> |
||||
|
@if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) { |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate" |
||||
|
class="tb-error"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} @else { |
||||
|
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}"> |
||||
|
@for (argument of argumentsList; track argument) { |
||||
|
<mat-option [value]="argument" [disabled]="argument === argumentInUse">{{ argument }}</mat-option> |
||||
|
} |
||||
|
</mat-select> |
||||
|
@if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').touched && filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').errors) { |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('required') ? 'calculated-fields.hint.argument-name-required' : 'alarm-rule.argument-in-use') | translate" |
||||
|
class="tb-error !block"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
} |
||||
|
@if (filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').hasError('argumentInUse')) { |
||||
|
<mat-icon matSuffix |
||||
|
matTooltipPosition="above" |
||||
|
matTooltipClass="tb-error-tooltip" |
||||
|
[matTooltip]="'alarm-rule.argument-in-use' | translate" |
||||
|
class="tb-error !block"> |
||||
|
warning |
||||
|
</mat-icon> |
||||
|
} |
||||
|
</mat-form-field> |
||||
|
} |
||||
|
</ng-container> |
||||
|
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> |
||||
|
<mat-select formControlName="unit" required placeholder="{{'alarm-rule.time-unit' | translate}}"> |
||||
|
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> |
||||
|
{{ timeUnitsTranslationMap.get(timeUnit) | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
@ -0,0 +1,166 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2025 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, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; |
||||
|
import { |
||||
|
ControlValueAccessor, |
||||
|
FormBuilder, |
||||
|
NG_VALIDATORS, |
||||
|
NG_VALUE_ACCESSOR, |
||||
|
ValidationErrors, |
||||
|
Validator, |
||||
|
Validators |
||||
|
} from '@angular/forms'; |
||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; |
||||
|
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; |
||||
|
import { TimeUnit, timeUnitTranslations } from "@home/components/rule-node/rule-node-config.models"; |
||||
|
import { AlarmRuleFilterPredicateType, NoDataAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; |
||||
|
import { isDefinedAndNotNull } from "@core/utils"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-alarm-rule-filter-predicate-no-data-value', |
||||
|
templateUrl: './alarm-rule-filter-predicate-no-data-value.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [ |
||||
|
{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), |
||||
|
multi: true |
||||
|
}, |
||||
|
{ |
||||
|
provide: NG_VALIDATORS, |
||||
|
useExisting: forwardRef(() => AlarmRuleFilterPredicateNoDataValueComponent), |
||||
|
multi: true |
||||
|
} |
||||
|
] |
||||
|
}) |
||||
|
export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValueAccessor, Validator, OnInit, OnChanges { |
||||
|
|
||||
|
@Input() |
||||
|
arguments: Record<string, CalculatedFieldArgument>; |
||||
|
|
||||
|
@Input() |
||||
|
valueType: AlarmRuleFilterPredicateType; |
||||
|
|
||||
|
@Input() |
||||
|
argumentInUse: string; |
||||
|
|
||||
|
valueTypeEnum = AlarmRuleFilterPredicateType; |
||||
|
|
||||
|
filterPredicateValueNoDataFormGroup = this.fb.group({ |
||||
|
type: ['NO_DATA'], |
||||
|
unit: [TimeUnit.MINUTES, Validators.required], |
||||
|
duration: this.fb.group({ |
||||
|
staticValue: [null as null | number, [Validators.required, Validators.min(1)]], |
||||
|
dynamicValueArgument: ['', Validators.required] |
||||
|
}) |
||||
|
}); |
||||
|
|
||||
|
timeUnits = [TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS]; |
||||
|
timeUnitsTranslationMap = timeUnitTranslations; |
||||
|
|
||||
|
|
||||
|
dynamicModeControl = this.fb.control(false); |
||||
|
|
||||
|
argumentsList: Array<string>; |
||||
|
|
||||
|
private propagateChange= (v: any) => { }; |
||||
|
|
||||
|
constructor(private fb: FormBuilder, |
||||
|
private destroyRef: DestroyRef) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.argumentsList = this.arguments ? Object.keys(this.arguments): []; |
||||
|
this.filterPredicateValueNoDataFormGroup.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(() => { |
||||
|
this.updateModel(); |
||||
|
}); |
||||
|
this.dynamicModeControl.valueChanges.pipe( |
||||
|
takeUntilDestroyed(this.destroyRef) |
||||
|
).subscribe(value => this.updateValueModeValidators(value)); |
||||
|
} |
||||
|
|
||||
|
ngOnChanges(changes: SimpleChanges) { |
||||
|
if (changes.argumentInUse) { |
||||
|
const argumentInUseChanges = changes.argumentInUse; |
||||
|
if (!argumentInUseChanges.firstChange && argumentInUseChanges.currentValue !== argumentInUseChanges.previousValue) { |
||||
|
if (this.dynamicModeControl.value) { |
||||
|
if (this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value) { |
||||
|
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true}); |
||||
|
this.filterPredicateValueNoDataFormGroup.updateValueAndValidity(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
if (isDisabled) { |
||||
|
this.filterPredicateValueNoDataFormGroup.disable({emitEvent: false}); |
||||
|
this.dynamicModeControl.disable({emitEvent: false}); |
||||
|
} else { |
||||
|
this.filterPredicateValueNoDataFormGroup.enable({emitEvent: false}); |
||||
|
this.dynamicModeControl.enable({emitEvent: false}); |
||||
|
this.updateValueModeValidators(this.dynamicModeControl.value); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private updateValueModeValidators(isDynamicMode: boolean): void { |
||||
|
if (isDynamicMode) { |
||||
|
this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').disable({emitEvent: false}); |
||||
|
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').enable(); |
||||
|
setTimeout(()=> { |
||||
|
if (this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').value && this.argumentInUse === this.filterPredicateValueNoDataFormGroup.get('dynamicValueArgument').value) { |
||||
|
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').setErrors({argumentInUse: true}); |
||||
|
this.filterPredicateValueNoDataFormGroup.updateValueAndValidity(); |
||||
|
} |
||||
|
}, 0); |
||||
|
} else { |
||||
|
this.filterPredicateValueNoDataFormGroup.get('duration.dynamicValueArgument').disable({emitEvent: false}); |
||||
|
this.filterPredicateValueNoDataFormGroup.get('duration.staticValue').enable(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
validate(): ValidationErrors | null { |
||||
|
return this.filterPredicateValueNoDataFormGroup.valid ? null : { |
||||
|
filterPredicateValue: {valid: false} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void { |
||||
|
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) { |
||||
|
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse); |
||||
|
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) { |
||||
|
predicateValue.duration.dynamicValueArgument = ''; |
||||
|
} |
||||
|
this.dynamicModeControl.patchValue(true, {emitEvent: false}); |
||||
|
} |
||||
|
this.filterPredicateValueNoDataFormGroup.patchValue(predicateValue, {emitEvent: false}); |
||||
|
} |
||||
|
|
||||
|
private updateModel() { |
||||
|
this.propagateChange(this.filterPredicateValueNoDataFormGroup.value); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue