Browse Source

Merge pull request #14512 from ArtemDzhereleiko/AD/cf-alarm-rule/bug-fix-and-enh

Bug-fix and enhancement for calculated fields and alarm rule
pull/14525/head
Vladyslav Prykhodko 9 months ago
committed by GitHub
parent
commit
86583eb9be
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 60
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  2. 62
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  3. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  4. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  5. 45
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  6. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  7. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  8. 50
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  9. 14
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  10. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  11. 115
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  12. 42
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  13. 92
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts
  15. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  16. 27
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  17. 60
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  18. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  19. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
  20. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  21. 18
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  22. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  23. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  24. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html
  25. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts
  26. 15
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html
  27. 16
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  28. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  29. 16
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.html
  30. 11
      ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss
  31. 4
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  32. 4
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
  33. 4
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  34. 70
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  35. 2
      ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md
  36. 30
      ui-ngx/src/assets/locale/locale.constant-en_US.json

60
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -29,10 +29,11 @@
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<div class="flex gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'alarm-rule.alarm-type' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
<mat-hint translate>alarm-rule.alarm-type-hint</mat-hint>
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) {
<mat-error>
@if (fieldFormGroup.get('name').hasError('required')) {
@ -48,15 +49,18 @@
<tb-entity-debug-settings-button
formControlName="debugSettings"
entityLabel="alarm-rule.alarm-rule"
class="pt-[10px]"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
@if (!data.entityId) {
<div class="flex flex-col gap-4" formGroupName="entityId">
<tb-entity-type-select appearance="outline"
<div class="flex flex-row xs:flex-col gap-4" formGroupName="entityId">
<tb-entity-type-select #entityTypeSelect
appearance="outline"
subscriptSizing="dynamic"
class="flex-1"
showLabel
required
label="{{ 'alarm-rule.target-entity-type' | translate }}"
@ -65,13 +69,14 @@
formControlName="entityType">
</tb-entity-type-select>
@if (fieldFormGroup.get('entityId.entityType').value) {
<tb-entity-autocomplete
formControlName="id"
appearance="outline"
subscriptSizing="dynamic"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
<tb-entity-autocomplete #entityAutocompleteComponent
formControlName="id"
appearance="outline"
subscriptSizing="dynamic"
class="flex-1"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
/>
}
</div>
@ -85,8 +90,8 @@
[tenantId]="data.tenantId"
[ownerId]="data.ownerId"
[watchKeyChange]="true"
[disabledAddButton]="!fieldFormGroup.get('entityId.id').value"
[entityName]="data.entityName"/>
[disable]="!fieldFormGroup.get('entityId.id').value || !fieldFormGroup.get('name').value"
[entityName]="entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
@ -113,11 +118,11 @@
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate style="margin: 16px 0"
class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
<span translate class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
[disabled]="!disabledClearRuleButton"
type="button"
(click)="addClearAlarmRule()">
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
@ -137,23 +142,14 @@
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block p-4" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
<tb-string-items-list appearance="outline"
class="flex flex-1 px-4"
label="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}"
placeholder="{{ 'alarm-rule.alarm-rule-relation-types-list' | translate }}"
hint="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"
[predefinedValues]="predefinedTypeValues"
formControlName="propagateRelationTypes">
</tb-string-items-list>
}
</div>
<div class="tb-form-row">
@ -181,7 +177,7 @@
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty">
[disabled]="isLoading$ | async">
{{ data.buttonTitle | translate }}
</button>
</div>

62
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -40,6 +40,11 @@ import {
import { deepTrim } from "@core/utils";
import { Observable } from "rxjs";
import { switchMap } from "rxjs/operators";
import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component";
import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component";
import { EntityService } from "@core/http/entity.service";
import { RelationTypes } from "@shared/models/relation.models";
import { StringItemsOption } from "@shared/components/string-items-list.component";
export interface AlarmRuleDialogData {
value?: CalculatedField;
@ -66,7 +71,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
type: [CalculatedFieldType.ALARM],
debugSettings: [],
entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required),
entityType: this.fb.control<EntityType | AliasEntityType | null>(EntityType.DEVICE_PROFILE, Validators.required),
id: [null as null | string, Validators.required],
}),
configuration: this.fb.group({
@ -93,15 +98,45 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
entityName = this.data.entityName;
disabledClearRuleButton = false;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
private entityService: EntityService,
private destroyRef: DestroyRef,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.applyDialogData();
this.updateRulesValidators();
this.fieldFormGroup.get('configuration.arguments').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateRulesValidators();
});
if (!this.entityName) {
this.fieldFormGroup.get('entityId.id').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((entityId) => {
if (entityId && (this.fieldFormGroup.get('entityId.entityType').value === EntityType.DEVICE_PROFILE ||
this.fieldFormGroup.get('entityId.entityType').value === EntityType.ASSET_PROFILE)) {
this.entityService.getEntity(this.fieldFormGroup.get('entityId.entityType').value as EntityType, entityId, {ignoreLoading: true, ignoreErrors: true}).subscribe(
value => {
this.entityName = value.name;
}
)
}
});
}
}
get configFormGroup(): FormGroup {
@ -169,6 +204,10 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
this.calculatedFieldsService.saveCalculatedField(alarmRule)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(calculatedField => this.dialogRef.close(calculatedField));
} else {
this.fieldFormGroup.get('name').markAsTouched();
this.entityTypeSelect.markAsTouched();
this.entityAutocompleteComponent.markAsTouched();
}
}
@ -191,4 +230,23 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false);
}
private updateRulesValidators(): void {
if (Object.keys(this.arguments ?? {}).length > 0) {
this.fieldFormGroup.get('configuration.createRules').enable({emitEvent: false});
this.fieldFormGroup.get('configuration.clearRule').enable({emitEvent: false});
this.disabledClearRuleButton = true;
} else {
this.fieldFormGroup.get('configuration.createRules').disable({emitEvent: false});
this.fieldFormGroup.get('configuration.clearRule').disable({emitEvent: false});
this.disabledClearRuleButton = false;
}
}
get predefinedTypeValues(): StringItemsOption[] {
return RelationTypes.map(type => ({
name: type,
value: type
}));
}
}

2
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts

@ -287,7 +287,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
}
private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog()
this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file')
.pipe(
filter(Boolean),
switchMap(calculatedField => {

4
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -75,14 +75,14 @@
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!argumentsList.length"
(click)="onTestScript()">
(click)="onTestScript($event)">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
(click)="onTestScript($event)"
[disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>

45
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts

@ -41,6 +41,7 @@ import {
alarmRuleDefaultScript,
AlarmRuleExpressionType,
AlarmRuleFilter,
areFiltersAndPredicateArgumentsValid,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
@ -137,7 +138,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
staticValue: this.condition?.value?.staticValue,
staticValue: this.condition?.value?.staticValue ?? null,
dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null,
},
count: {
@ -150,7 +151,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
this.filtersValid = this.areFilterAndPredicateArgumentsValid(this.condition?.expression?.filters, this.argumentsList);
this.filtersValid = areFiltersAndPredicateArgumentsValid(this.condition?.expression?.filters, this.data.arguments);
this.checkIsNoData(this.condition?.expression?.filters);
this.conditionFormGroup.get('type').valueChanges.pipe(
@ -168,7 +169,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.conditionFormGroup.get('expression.filters').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((filters) => {
this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList);
this.filtersValid = areFiltersAndPredicateArgumentsValid(filters, this.data.arguments);
this.checkIsNoData(filters);
});
@ -206,39 +207,6 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
const filters = obj || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
}
return true;
}
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
@ -324,7 +292,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
if (value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${value.dynamicValueArgument}`
});
}) + ' ' + this.translate.instant(this.timeUnitTranslations.get(this.conditionFormGroup.get('unit').value)).toLowerCase();
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText
@ -355,7 +323,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
}
onTestScript() {
onTestScript($event: Event) {
$event?.preventDefault();
this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe(
(expression) => {
this.conditionFormGroup.get('expression.expression').setValue(expression);

9
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -26,13 +26,15 @@
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
[arguments]="arguments"
[disabled]="disabled"
class="flex-1"
[nowrap]="true"
[specText]="specText"
required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
<mat-icon [color]="disabled ? '' : (conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn')"
class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
{{ conditionSet() ? 'edit' : 'add' }}
</mat-icon>
</div>
@ -48,7 +50,10 @@
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon [color]="schedulerArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
<mat-icon [color]="disabled ? '' : (schedulerArgumentsValid ? 'primary' : 'warn')"
class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
edit
</mat-icon>
</div>
</button>
</div>

30
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -32,7 +32,7 @@ import {
getAlarmScheduleRangeText,
utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { TimeUnit } from '@shared/models/time/time.models';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import {
CfAlarmRuleConditionDialogComponent,
CfAlarmRuleConditionDialogData
@ -42,7 +42,8 @@ import {
AlarmRuleConditionType,
AlarmRuleExpressionType,
AlarmRuleSchedule,
AlarmRuleScheduleType
AlarmRuleScheduleType,
checkPredicates
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import {
@ -154,32 +155,17 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
return !arg || validArguments.includes(arg);
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
private areFilterAndPredicateArgumentsValid(obj: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
const filters = obj?.expression?.filters || obj?.filters || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
if (!checkPredicates(filter.predicates, validSet)) {
return false;
}
}
@ -192,7 +178,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
}
public validate(control: AbstractControl): ValidationErrors | null {
this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments));
this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, this.arguments);
this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true });
@ -265,7 +251,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
if (this.modelValue.value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${this.modelValue.value.dynamicValueArgument}`
});
}) + ' ' + this.translate.instant(timeUnitTranslationMap.get(this.modelValue.unit)).toLowerCase();
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText

50
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html

@ -18,33 +18,29 @@
<div class="tb-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup">
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition">
</tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-additional-info-icon-hint' | translate }}" translate>
alarm-rule.alarm-rule-additional-info
</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
<mat-icon class="material-icons">open_in_new</mat-icon>
</button>
</mat-form-field>
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-additional-info-icon-hint' | translate }}" translate>
alarm-rule.alarm-rule-additional-info
</div>
}
@if (!disabled || alarmRuleFormGroup.get('dashboardId').value) {
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate>
alarm-rule.alarm-rule-mobile-dashboard
</div>
<tb-dashboard-autocomplete appearance="outline"
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
<mat-icon class="material-icons">open_in_new</mat-icon>
</button>
</mat-form-field>
</div>
<div class="tb-form-row space-between column-xs">
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate>
alarm-rule.alarm-rule-mobile-dashboard
</div>
}
<tb-dashboard-autocomplete appearance="outline"
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
</div>
</div>

14
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html

@ -16,7 +16,7 @@
-->
<div class="flex flex-1 flex-col">
<div class="flex flex-1 flex-col gap-4">
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
<div class="flex flex-row items-center justify-start gap-2 pb-2" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule flex flex-1 flex-col gap-4"
@ -48,14 +48,14 @@
</button>
</div>
}
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled">
<span translate class="flex items-center justify-center tb-prompt required" style="margin: 16px 0">
@if (!createAlarmRulesFormArray().controls.length) {
<span translate class="flex items-center justify-center tb-prompt required" [class.required]="!disabled">
alarm-rule.add-create-alarm-rule-prompt
</span>
</div>
<div *ngIf="!disabled">
</span>
}
<div>
<button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4"
[disabled]="createAlarmRulesFormArray().controls.length > 4 || disabled"
type="button"
(click)="addCreateAlarmRule()">
{{ 'alarm-rule.add-create-alarm-rule' | translate }}

33
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts

@ -27,12 +27,12 @@
import {
AlarmRuleFilter,
AlarmRuleFilterPredicate,
filterOperationTranslationMap
filterOperationTranslationMap,
isPredicateArgumentsValid
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { isDefinedAndNotNull } from "@core/utils";
export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter;
@ -87,11 +87,11 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
}
);
this.predicatesValid = this.isPredicateArgumentsValid(this.data.filter.predicates);
this.predicatesValid = isPredicateArgumentsValid(this.data.filter.predicates, this.arguments);
this.filterFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicatesValid = this.isPredicateArgumentsValid(predicates);
this.predicatesValid = isPredicateArgumentsValid(predicates, this.arguments);
});
this.filterFormGroup.get('valueType').valueChanges.pipe(
@ -116,31 +116,6 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
});
}
private isPredicateArgumentsValid(predicates: any): boolean {
const validSet = new Set(Object.keys(this.data.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (isDefinedAndNotNull(p.value?.dynamicValueArgument)) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
return false;
}
}
return true;
}
argumentInUse(argument: string): boolean {
return this.data.usedArguments.includes(argument);
}

115
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html

@ -15,69 +15,68 @@
limitations under the License.
-->
<section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span class="min-w-24">&nbsp;</span>
@if (filtersFormArray.length) {
<section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span class="min-w-24">&nbsp;</span>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="filter-list">
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start"
formArrayName="filters"
[class.filter-list-divider]="index">
<div class="filters-operation max-w-8% flex-full">
@if ($index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
<mat-divider></mat-divider>
<div class="filter-list">
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start"
formArrayName="filters"
[class.filter-list-divider]="index">
<div class="filters-operation max-w-8% flex-full">
@if ($index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</div>
}
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value, arguments)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
}
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1 filters-text">{{ filterControl.value?.argument }}</div>
<div class="flex-1 filters-text">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary" class="tb-budge-button"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon
[matBadgeHidden]="areFilterAndPredicateArgumentsValid(filterControl.value)"
matBadgeColor="warn"
matBadgeSize="small"
matBadge="*"
>
edit
</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
@if (index) {
<mat-divider></mat-divider>
}
</div>
@if (index) {
<mat-divider></mat-divider>
}
</div>
}
@if (!filtersFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
</div>
</section>
}
</div>
</section>
} @else {
<span class="no-data-found tb-prompt flex items-center justify-center" [class.required]="!disabled" translate>alarm-rule.no-filter</span>
}
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addFilter()"
type="button">
{{ 'alarm-rule.add-filter' | translate }}

42
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts

@ -38,7 +38,11 @@ import {
AlarmRuleFilterDialogComponent,
AlarmRuleFilterDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
import { AlarmRuleFilter, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models";
import {
AlarmRuleFilter,
areFilterAndPredicateArgumentsValid,
FilterPredicateTypeTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@ -71,6 +75,10 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
filters: this.fb.array([])
});
disabled = false;
areFilterAndPredicateArgumentsValid = areFilterAndPredicateArgumentsValid;
complexOperationTranslationMap = complexOperationTranslationMap;
FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap
@ -102,37 +110,8 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
};
}
public areFilterAndPredicateArgumentsValid(obj: any): boolean {
const validSet = new Set(Object.keys(this.arguments));
const filter = obj || [];
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) {
return false;
}
}
return true;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
@ -206,5 +185,4 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
const filters = this.filterListFormGroup.value.filters as Array<AlarmRuleFilter>;
this.propagateChange(filters);
}
}

92
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html

@ -16,66 +16,62 @@
-->
<section class="flex flex-col gap-4" [formGroup]="filterListFormGroup">
<div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row">
<div class="flex flex-full flex-row items-center justify-start gap-2">
<div class="flex flex-1 flex-row gap-4 pl-2">
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label>
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label>
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label>
@if (predicatesFormArray.length) {
<div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row">
<div class="flex flex-full flex-row items-center justify-start gap-2">
<div class="flex flex-1 flex-row gap-4 pl-2">
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label>
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label>
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label>
</div>
<span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div>
<span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div>
</div>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start relative"
[class.key-filter-list-divider]="$index">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
}
<div class="flex flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start">
<tb-alarm-rule-filter-predicate
class="flex-1"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-alarm-rule-filter-predicate>
<button mat-icon-button color="primary"
[disabled]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start relative"
[class.key-filter-list-divider]="$index">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
}
<div class="flex flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start">
<tb-alarm-rule-filter-predicate
class="flex-1"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-alarm-rule-filter-predicate>
<button mat-icon-button color="primary"
[disabled]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
</div>
}
@if (!predicatesFormArray.length) {
<span class="no-data-found tb-prompt flex items-center justify-center" translate>
alarm-rule.no-filter
</span>
<tb-error noMargin [error]="'alarm-rule.filter-required' | translate" class="flex h-9 items-center pl-3"/>
}
}
</div>
</div>
</div>
<div class="flex flex-row gap-2">
} @else {
<span class="no-data-found tb-prompt flex items-center justify-center" [class.required]="!disabled" translate>alarm-rule.no-filter</span>
}
<div class="flex flex-row gap-2" [class.!hidden]="disabled">
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button
color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button">
{{ 'filter.add-complex' | translate }}

2
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts

@ -150,7 +150,7 @@ export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValu
}
writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void {
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) {
if (isDefinedAndNotNull(predicateValue?.duration?.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) {
predicateValue.duration.dynamicValueArgument = '';

2
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts

@ -166,7 +166,7 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
if (isDefinedAndNotNull(predicateValue.dynamicValueArgument)) {
if (isDefinedAndNotNull(predicateValue?.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.dynamicValueArgument)) {
predicateValue.dynamicValueArgument = '';

27
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html

@ -70,20 +70,17 @@
</div>
}
}
@if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) {
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="duration">
</tb-alarm-rule-filter-predicate-no-data-value>
} @else if (type !== filterPredicateType.COMPLEX) {
<tb-alarm-rule-filter-predicate-value class="flex-full"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="value">
</tb-alarm-rule-filter-predicate-value>
}
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full" [class.!hidden]="!(filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA)"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="duration">
</tb-alarm-rule-filter-predicate-no-data-value>
<tb-alarm-rule-filter-predicate-value class="flex-full" [class.!hidden]="!(filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA && type !== filterPredicateType.COMPLEX)"
[arguments]="arguments"
[argumentInUse]="argumentInUse"
[valueType]="valueType"
formControlName="value">
</tb-alarm-rule-filter-predicate-value>
</div>
</div>

60
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts

@ -34,6 +34,7 @@ import {
alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap,
checkPredicates,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { MatDialog } from "@angular/material/dialog";
@ -111,6 +112,18 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
this.updateModel();
});
this.filterPredicateFormGroup.get('operation').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => {
if (value === 'NO_DATA') {
this.filterPredicateFormGroup.get('duration').enable({emitEvent: false});
this.filterPredicateFormGroup.get('value').disable({emitEvent: false});
} else {
this.filterPredicateFormGroup.get('duration').disable({emitEvent: false});
this.filterPredicateFormGroup.get('value').enable({emitEvent: false});
}
})
this.filterPredicateFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
@ -140,25 +153,10 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
}
}
private isPredicateArgumentsValid(predicates: any): boolean {
private isPredicateArgumentsValid(predicates: AlarmRuleFilterPredicate[]): boolean {
const validSet = new Set(Object.keys(this.arguments));
function checkPredicates(predicates: any[]): boolean {
for (const p of predicates) {
if (p.value?.dynamicValueArgument) {
if (!validSet.has(p.value.dynamicValueArgument)) {
return false;
}
}
if (p.type === 'COMPLEX' && Array.isArray(p.predicates)) {
if (!checkPredicates(p.predicates)) {
return false;
}
}
}
return true;
}
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) {
if (!checkPredicates(predicates, validSet)) {
return false;
}
}
@ -172,8 +170,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
}
if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) {
this.type = AlarmRuleFilterPredicateType[this.valueType];
this.filterPredicateFormGroup.get('duration').enable({emitEvent: false});
this.filterPredicateFormGroup.get('value').disable({emitEvent: false});
this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false});
} else {
this.filterPredicateFormGroup.get('duration').disable({emitEvent: false});
this.filterPredicateFormGroup.get('value').enable({emitEvent: false});
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
}
}
@ -183,30 +185,6 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
if (predicate.operation === 'NO_DATA') {
this.propagateChange(predicate.duration);
} else {
if (!predicate.value) {
switch (this.valueType) {
case EntityKeyValueType.STRING:
predicate.value = {
staticValue: ''
};
break;
case EntityKeyValueType.NUMERIC:
predicate.value = {
staticValue: 0
};
break;
case EntityKeyValueType.DATE_TIME:
predicate.value = {
staticValue: Date.now()
};
break;
case EntityKeyValueType.BOOLEAN:
predicate.value = {
staticValue: false
};
break;
}
}
this.propagateChange({type: this.type, ...predicate});
}
}

1
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html

@ -18,6 +18,7 @@
<div class="tb-filter-text"
tbTruncateWithTooltip
[class.required]="isRequired"
[class.disabled]="disabled"
[class.nowrap]="nowrap"
[class.wrap]="!nowrap"
[innerHTML]="filterText">

3
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss

@ -23,6 +23,9 @@
color: #f44336;
padding: 0;
}
&.disabled {
color: rgba(0,0,0,0.38);
}
&.nowrap {
white-space: nowrap;
text-overflow: ellipsis;

4
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts

@ -63,6 +63,10 @@ export class AlarmRuleFilterTextComponent {
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
disabled = false;
private alarmRuleExpressionValue: AlarmRuleExpression;
get alarmRuleExpression(): AlarmRuleExpression {
return this.alarmRuleExpressionValue;

18
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html

@ -15,8 +15,10 @@
limitations under the License.
-->
<div class="flex flex-col gap-3">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<div class="flex flex-col gap-4">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col"
[class.arguments-table-with-error]="errorText"
[class.!hidden]="!(dataSource.isEmpty() | async) === false">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
@ -94,6 +96,7 @@
<button type="button"
mat-icon-button
#button
[disabled]="disable"
(click)="manageArgument($event, button, argument)"
[matTooltip]="'action.edit' | translate"
matTooltipPosition="above">
@ -119,14 +122,15 @@
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayColumns"></mat-header-row>
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center min-h-14">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
@if (errorText || (dataSource.isEmpty() | async)) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
[class.required]="!disable"
class="tb-prompt flex flex-1 items-end justify-center tb-prompt required">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
<div class="flex h-9 justify-between">
<button
type="button"
@ -134,7 +138,7 @@
color="primary"
#button
(click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disabledAddButton"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disable"
>
{{ 'calculated-fields.add-argument' | translate }}
</button>

5
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts

@ -16,6 +16,7 @@
import {
AfterViewInit,
booleanAttribute,
ChangeDetectorRef,
Component,
DestroyRef,
@ -87,7 +88,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() entityName: string;
@Input() ownerId: EntityId;
@Input() isScript: boolean;
@Input() disabledAddButton = false;
@Input({transform: booleanAttribute}) disable = false;
@Input() watchKeyChange = false;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -220,8 +221,6 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
} else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
} else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty';
} else {
this.errorText = '';
}

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex size-full max-w-4xl flex-col">
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex max-w-4xl flex-col">
<mat-toolbar color="primary">
<h2>{{ 'entity.type-calculated-field' | translate}}</h2>
<span class="flex-1"></span>

12
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html

@ -16,7 +16,9 @@
-->
<div class="flex flex-col gap-3">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col"
[class.arguments-table-with-error]="errorText"
[class.!hidden]="!(dataSource.isEmpty() | async) === false">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
@ -121,14 +123,14 @@
*matHeaderRowDef="['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-header-row>
<mat-row *matRowDef="let argument; columns: ['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center">
{{ 'calculated-fields.no-zone-configured' | translate }}
</div>
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center tb-prompt required">
{{ 'calculated-fields.no-zone-configured' | translate }}
</div>
<div class="flex h-9 justify-between">
<button type="button"
mat-stroked-button

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts

@ -195,8 +195,6 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro
private updateErrorText(): void {
if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.geofencing-entity-not-found';
} else if (!this.zoneGroupsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.geofencing-empty';
} else {
this.errorText = '';
}

15
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html

@ -16,7 +16,9 @@
-->
<div class="flex flex-col gap-3">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col" [class.arguments-table-with-error]="errorText">
<div class="tb-form-panel stroked no-padding no-gap arguments-table flex flex-col"
[class.arguments-table-with-error]="errorText"
[class.!hidden]="!(dataSource.isEmpty() | async) === false">
<table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'">
@ -98,13 +100,10 @@
<mat-header-row class="mat-row-select" *matHeaderRowDef=displayColumns></mat-header-row>
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center">
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div>
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center required">
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
</div>
<div class="flex h-9 justify-between">
<button type="button"

16
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html

@ -72,12 +72,14 @@
</tb-time-unit-input>
</div>
<tb-calculate-field-output formControlName="output" [entityId]="entityId" simpleMode hiddenName>
<div class="tb-form-row simpleMode flex-1">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div>
</div>
</mat-slide-toggle>
</div>
@if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) {
<div class="tb-form-row simpleMode flex-1">
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div>
</div>
</mat-slide-toggle>
</div>
}
</tb-calculate-field-output>
</div>

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html

@ -23,6 +23,7 @@
[tenantId]="tenantId"
[ownerId]="ownerId"
[entityName]="entityName"
[watchKeyChange]="true"
[isScript]="isScript" />
</div>
<div class="tb-form-panel no-gap">

16
ui-ngx/src/app/modules/home/components/relation/relation-table.component.html

@ -104,8 +104,20 @@
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation">
{{ relation.type }}
<mat-cell *matCellDef="let relation" class="type">
<div class="flex flex-row" style="align-items: center">
<span class="ellipsis">{{ relation.type }}</span>
<tb-copy-button
class="type-copy"
[disabled]="isLoading$ | async"
[copyText]="relation.type"
tooltipText="{{ 'relation.copy-type' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
</div>
</mat-cell>
</ng-container>
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toEntityTypeName">

11
ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss

@ -59,6 +59,17 @@
overflow: hidden;
text-overflow: ellipsis;
}
.type-copy {
visibility: hidden;
transition: visibility 0.1s;
}
.type:hover {
.type-copy {
visibility: visible;
}
}
}
}

4
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -475,4 +475,8 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
get showEntityLink(): boolean {
return this.selectEntityFormGroup.get('entity').value && this.disabled && this.entityURL !== '';
}
markAsTouched(): void {
this.selectEntityFormGroup.get('entity').markAsTouched();
}
}

4
ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts

@ -173,4 +173,8 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
return '';
}
}
markAsTouched(): void {
this.entityTypeFormGroup.get('entityType').markAsTouched();
}
}

4
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -183,8 +183,8 @@ export class ImportExportService {
});
}
public openCalculatedFieldImportDialog(): Observable<CalculatedField> {
return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe(
public openCalculatedFieldImportDialog(importTitle = 'calculated-fields.import', importFileLabel = 'calculated-fields.file'): Observable<CalculatedField> {
return this.openImportDialog(importTitle, importFileLabel).pipe(
catchError(() => of(null)),
);
}

70
ui-ngx/src/app/shared/models/alarm-rule.models.ts

@ -18,17 +18,10 @@
import { CustomTimeSchedulerItem } from "@shared/models/device.models";
import { DashboardId } from "@shared/models/id/dashboard-id";
import { TimeUnit } from "@shared/models/time/time.models";
import {
BooleanOperation,
ComplexOperation,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
StringOperation
} from "@shared/models/query/query.models";
import { ComplexOperation, EntityKeyValueType, FilterPredicateType } from "@shared/models/query/query.models";
import { EntityType } from "@shared/models/entity-type.models";
import { Observable } from "rxjs";
import { CalculatedField } from "@shared/models/calculated-field.models";
import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export enum AlarmRuleScheduleType {
ANY_TIME = 'ANY_TIME',
@ -250,3 +243,62 @@ export const alarmRuleDefaultScript =
'return temperature > 20;'
export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, expression: string, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;
export function checkPredicates(predicates: any[], validSet: Set<string>): boolean {
for (const predicate of predicates) {
if (!predicate) continue;
if (predicate?.value?.dynamicValueArgument) {
if (!validSet.has(predicate.value.dynamicValueArgument)) {
return false;
}
}
if (predicate.type === 'COMPLEX' && Array.isArray(predicate.predicates)) {
if (!checkPredicates(predicate.predicates, validSet)) {
return false;
}
}
}
return true;
}
export function areFilterAndPredicateArgumentsValid(obj: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
const filter = obj || [];
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates, validSet)) {
return false;
}
}
return true;
}
export function areFiltersAndPredicateArgumentsValid(obj: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
const filters = obj || [];
for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) {
return false;
}
}
for (const filter of filters) {
if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates, validSet)) {
return false;
}
}
}
return true;
}
export function isPredicateArgumentsValid(predicates: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(Object.keys(args));
if (Array.isArray(predicates)) {
if (!checkPredicates(predicates, validSet)) {
return false;
}
}
return true;
}

2
ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md

@ -49,7 +49,7 @@ The argument value for a specific time schedule must be a JSON object in the fol
```javascript
{
"type": "CUSTOM"
"type": "CUSTOM",
"timezone": "Europe/Kiev",
"items": [
{

30
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1122,7 +1122,7 @@
"add-argument": "Add argument",
"test-script-function": "Test script function",
"test-expression-function": "Test expression function",
"no-arguments": "No arguments configured",
"no-arguments": "At least one argument is required.",
"argument-settings": "Argument settings",
"argument-current": "Current entity",
"argument-current-tenant": "Current tenant",
@ -1176,7 +1176,7 @@
"target-zone": "Target zone",
"perimeter-key": "Perimeter key",
"report-strategy": "Report strategy",
"no-zone-configured": "No zone group configured",
"no-zone-configured": "At least one zone is required.",
"no-zone-configured-required": "At least one zone group must be configured.",
"add-zone-group": "Add zone group",
"report-transition-event-only": "Transition events only",
@ -1245,7 +1245,7 @@
"key": "Key",
"function": "Function"
},
"no-metrics-configured": "No metrics configured",
"no-metrics-configured": "At least one metric is required.",
"add-metric": "Add metric",
"max-metrics": "Maximum number of metrics reached.",
"metric-settings": "Metric settings",
@ -1268,7 +1268,7 @@
"ttl-min": "Only 0 minimum TTL is allowed",
"processing-parameters": "Processing parameters",
"hint": {
"strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing",
"strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing.",
"processing-options": "Processing options",
"update-attribute-only-on-value-change": "Updates attribute on every incoming message, regardless of whether the value has changed. This increases API usage and reduces performance.",
"update-attribute-only-on-value-change-enabled": "Updates attribute only when the value changes. If the value is unchanged, timestamps are not updated and notifications are not sent.",
@ -1380,7 +1380,7 @@
"alarm-rules-old": "Old",
"alarm-rules-actual": "Actual",
"severities": "Severities",
"cleared": "Clearing condition",
"cleared": "Clear condition",
"delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?",
"delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.",
"delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?",
@ -1392,6 +1392,7 @@
"list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }",
"selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected",
"import": "Import alarm rule",
"file": "Alarm rule file",
"export": "Export alarm rule",
"export-failed-error": "Unable to export alarm rule: {{error}}",
"entity-type": "Entity type",
@ -1400,6 +1401,7 @@
"target-entity": "Target entity",
"target-entities": "Target entities",
"alarm-type": "Alarm type",
"alarm-type-hint": "Unique identifier (e.g., HighTemperatureAlarm) across the scope of the alarm originator (Device, Asset, etc.) to prevent conflicts.",
"alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
"alarm-type-max-length": "Alarm type should be less than 256 characters.",
@ -1435,8 +1437,7 @@
"add-filter": "Add argument filter",
"edit-filter": "Argument filter",
"remove-filter": "Remove argument filter",
"no-filter": "No argument filters configured",
"filter-required": "At least one filter must be configured.",
"no-filter": "At least one filter is required.",
"conditions": {
"simple": "Simple",
"duration": "Duration",
@ -1495,9 +1496,9 @@
"condition-type": "Condition type",
"condition-type-hint": "\"Duration\" and \"Repeating\" options are not available when the \"Missing for\" operation is used in the filter.",
"select-alarm-severity": "Select alarm severity",
"add-create-alarm-rule-prompt": "At least one creation condition should be configured",
"add-create-alarm-rule": "Add creation condition",
"add-clear-alarm-rule": "Add clearing condition",
"add-create-alarm-rule-prompt": "At least one trigger condition is required.",
"add-create-alarm-rule": "Add trigger condition",
"add-clear-alarm-rule": "Add clear condition",
"condition-duration": "Condition duration",
"condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit",
@ -1510,9 +1511,9 @@
"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.",
"create-conditions": "Creation conditions",
"clear-condition": "Clearing condition",
"no-clear-alarm-rule": "Clearing condition not configured",
"create-conditions": "Alarm Trigger Conditions",
"clear-condition": "Alarm Clear Condition",
"no-clear-alarm-rule": "No clear condition configured.",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities",
"alarm-rule-relation-types-list": "Relation types",
@ -4981,7 +4982,8 @@
"additional-info": "Additional info (JSON)",
"invalid-additional-info": "Unable to parse additional info json.",
"no-relations-text": "No relations found",
"not": "Not"
"not": "Not",
"copy-type": "Copy type"
},
"resource": {
"add": "Add resource",

Loading…
Cancel
Save