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