Browse Source

UI: Bug-fix and enh for cf

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

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

@ -14,7 +14,7 @@
/// limitations under the License. /// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -40,6 +40,11 @@ import {
import { deepTrim } from "@core/utils"; import { deepTrim } from "@core/utils";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { switchMap } from "rxjs/operators"; 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 { export interface AlarmRuleDialogData {
value?: CalculatedField; value?: CalculatedField;
@ -66,7 +71,7 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
type: [CalculatedFieldType.ALARM], type: [CalculatedFieldType.ALARM],
debugSettings: [], debugSettings: [],
entityId: this.fb.group({ 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], id: [null as null | string, Validators.required],
}), }),
configuration: this.fb.group({ configuration: this.fb.group({
@ -93,15 +98,45 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
separatorKeysCodes = [ENTER, COMMA, SEMICOLON]; separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
entityName = this.data.entityName;
disabledClearRuleButton = false;
@ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent;
@ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>, protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService, private calculatedFieldsService: CalculatedFieldsService,
private entityService: EntityService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.applyDialogData(); 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 { get configFormGroup(): FormGroup {
@ -169,6 +204,10 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
this.calculatedFieldsService.saveCalculatedField(alarmRule) this.calculatedFieldsService.saveCalculatedField(alarmRule)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(calculatedField => this.dialogRef.close(calculatedField)); .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); 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 { private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog() this.importExportService.openCalculatedFieldImportDialog('alarm-rule.import', 'alarm-rule.file')
.pipe( .pipe(
filter(Boolean), filter(Boolean),
switchMap(calculatedField => { 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" matTooltipPosition="above"
class="tb-mat-32" class="tb-mat-32"
[disabled]="!argumentsList.length" [disabled]="!argumentsList.length"
(click)="onTestScript()"> (click)="onTestScript($event)">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon> <mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button> </button>
</tb-js-func> </tb-js-func>
<div> <div>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
type="button" type="button"
(click)="onTestScript()" (click)="onTestScript($event)"
[disabled]="!argumentsList.length"> [disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }} {{ 'calculated-fields.test-script-function' | translate }}
</button> </button>

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

@ -41,6 +41,7 @@ import {
alarmRuleDefaultScript, alarmRuleDefaultScript,
AlarmRuleExpressionType, AlarmRuleExpressionType,
AlarmRuleFilter, AlarmRuleFilter,
areFiltersAndPredicateArgumentsValid,
filterOperationTranslationMap filterOperationTranslationMap
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
@ -137,7 +138,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE, type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
unit: this.condition?.unit ?? TimeUnit.SECONDS, unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: { 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, dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null,
}, },
count: { count: {
@ -150,7 +151,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false}); this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.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.checkIsNoData(this.condition?.expression?.filters);
this.conditionFormGroup.get('type').valueChanges.pipe( this.conditionFormGroup.get('type').valueChanges.pipe(
@ -168,7 +169,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.conditionFormGroup.get('expression.filters').valueChanges.pipe( this.conditionFormGroup.get('expression.filters').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe((filters) => { ).subscribe((filters) => {
this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList); this.filtersValid = areFiltersAndPredicateArgumentsValid(filters, this.data.arguments);
this.checkIsNoData(filters); 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') { updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') { if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false}); this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
@ -324,7 +292,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
if (value.dynamicValueArgument) { if (value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${value.dynamicValueArgument}` attribute: `${value.dynamicValueArgument}`
}); }) + ' ' + this.translate.instant(this.timeUnitTranslations.get(this.conditionFormGroup.get('unit').value)).toLowerCase();
} else { } else {
this.specText = this.translate.instant('alarm-rule.condition-during', { this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText during: duringText
@ -355,7 +323,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition); this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
} }
onTestScript() { onTestScript($event: Event) {
$event?.preventDefault();
this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe( this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe(
(expression) => { (expression) => {
this.conditionFormGroup.get('expression.expression').setValue(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"> <div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value" <tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
[arguments]="arguments" [arguments]="arguments"
[disabled]="disabled"
class="flex-1" class="flex-1"
[nowrap]="true" [nowrap]="true"
[specText]="specText" [specText]="specText"
required required
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}"> addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text> </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' }} {{ conditionSet() ? 'edit' : 'add' }}
</mat-icon> </mat-icon>
</div> </div>
@ -48,7 +50,10 @@
(click)="openScheduleDialog($event)"> (click)="openScheduleDialog($event)">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span> <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> </div>
</button> </button>
</div> </div>

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

@ -32,7 +32,7 @@ import {
getAlarmScheduleRangeText, getAlarmScheduleRangeText,
utcTimestampToTimeOfDay utcTimestampToTimeOfDay
} from '@shared/models/device.models'; } from '@shared/models/device.models';
import { TimeUnit } from '@shared/models/time/time.models'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { import {
CfAlarmRuleConditionDialogComponent, CfAlarmRuleConditionDialogComponent,
CfAlarmRuleConditionDialogData CfAlarmRuleConditionDialogData
@ -42,7 +42,8 @@ import {
AlarmRuleConditionType, AlarmRuleConditionType,
AlarmRuleExpressionType, AlarmRuleExpressionType,
AlarmRuleSchedule, AlarmRuleSchedule,
AlarmRuleScheduleType AlarmRuleScheduleType,
checkPredicates
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { import {
@ -154,32 +155,17 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
return !arg || validArguments.includes(arg); return !arg || validArguments.includes(arg);
} }
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean { private areFilterAndPredicateArgumentsValid(obj: any, args: Record<string, CalculatedFieldArgument>): boolean {
const validSet = new Set(validArguments); const validSet = new Set(Object.keys(args));
const filters = obj?.expression?.filters || obj?.filters || []; const filters = obj?.expression?.filters || obj?.filters || [];
for (const filter of filters) { for (const filter of filters) {
if (filter.argument && !validSet.has(filter.argument)) { if (filter.argument && !validSet.has(filter.argument)) {
return false; 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) { for (const filter of filters) {
if (Array.isArray(filter.predicates)) { if (Array.isArray(filter.predicates)) {
if (!checkPredicates(filter.predicates)) { if (!checkPredicates(filter.predicates, validSet)) {
return false; return false;
} }
} }
@ -192,7 +178,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
} }
public validate(control: AbstractControl): ValidationErrors | null { 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.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => { this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true }); control.updateValueAndValidity({ emitEvent: true });
@ -265,7 +251,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
if (this.modelValue.value.dynamicValueArgument) { if (this.modelValue.value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', { this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${this.modelValue.value.dynamicValueArgument}` attribute: `${this.modelValue.value.dynamicValueArgument}`
}); }) + ' ' + this.translate.instant(timeUnitTranslationMap.get(this.modelValue.unit)).toLowerCase();
} else { } else {
this.specText = this.translate.instant('alarm-rule.condition-during', { this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText 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"> <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 formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition">
</tb-cf-alarm-rule-condition> </tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) { <div class="tb-form-row space-between column-xs">
<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>
<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
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> </div>
} <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
@if (!disabled || alarmRuleFormGroup.get('dashboardId').value) { <input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<div class="tb-form-row space-between column-xs"> <button type="button"
<div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate> matSuffix mat-icon-button aria-label="Open in new"
alarm-rule.alarm-rule-mobile-dashboard (click)="openEditDetailsDialog($event)">
</div> <mat-icon class="material-icons">open_in_new</mat-icon>
<tb-dashboard-autocomplete appearance="outline" </button>
subscriptSizing="dynamic" </mat-form-field>
inlineField </div>
class="flex-1" <div class="tb-form-row space-between column-xs">
placeholder="{{ 'action.set' | translate }}" <div class="min-w-40 xs:min-w-fit" tb-hint-tooltip-icon="{{'alarm-rule.alarm-rule-mobile-dashboard-hint' | translate}}" translate>
formControlName="dashboardId"> alarm-rule.alarm-rule-mobile-dashboard
</tb-dashboard-autocomplete>
</div> </div>
} <tb-dashboard-autocomplete appearance="outline"
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
</div>
</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) { @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="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" <div class="create-alarm-rule flex flex-1 flex-col gap-4"
@ -48,14 +48,14 @@
</button> </button>
</div> </div>
} }
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled"> @if (!createAlarmRulesFormArray().controls.length) {
<span translate class="flex items-center justify-center tb-prompt required" style="margin: 16px 0"> <span translate class="flex items-center justify-center tb-prompt required" [class.required]="!disabled">
alarm-rule.add-create-alarm-rule-prompt alarm-rule.add-create-alarm-rule-prompt
</span> </span>
</div> }
<div *ngIf="!disabled"> <div>
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4" [disabled]="createAlarmRulesFormArray().controls.length > 4 || disabled"
type="button" type="button"
(click)="addCreateAlarmRule()"> (click)="addCreateAlarmRule()">
{{ 'alarm-rule.add-create-alarm-rule' | translate }} {{ '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 { import {
AlarmRuleFilter, AlarmRuleFilter,
AlarmRuleFilterPredicate, AlarmRuleFilterPredicate,
filterOperationTranslationMap filterOperationTranslationMap,
isPredicateArgumentsValid
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { FormControlsFrom } from "@shared/models/tenant.model"; import { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { isDefinedAndNotNull } from "@core/utils";
export interface AlarmRuleFilterDialogData { export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter; 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( this.filterFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => { ).subscribe(predicates => {
this.predicatesValid = this.isPredicateArgumentsValid(predicates); this.predicatesValid = isPredicateArgumentsValid(predicates, this.arguments);
}); });
this.filterFormGroup.get('valueType').valueChanges.pipe( 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 { argumentInUse(argument: string): boolean {
return this.data.usedArguments.includes(argument); 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. limitations under the License.
--> -->
<section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup"> @if (filtersFormArray.length) {
<div class="flex flex-row"> <section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<span class="max-w-8% flex-full"></span> <div class="flex flex-row">
<div class="flex max-w-92% flex-full flex-row items-center justify-start"> <span class="max-w-8% flex-full"></span>
<label translate class="filter-title flex-1">alarm-rule.argument-name</label> <div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.value-type</label> <label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<span class="min-w-24">&nbsp;</span> <label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span class="min-w-24">&nbsp;</span>
</div>
</div> </div>
</div> <mat-divider></mat-divider>
<mat-divider></mat-divider> <div class="filter-list">
<div class="filter-list"> @for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) { <div class="flex flex-row items-stretch justify-start"
<div class="flex flex-row items-stretch justify-start" formArrayName="filters"
formArrayName="filters" [class.filter-list-divider]="index">
[class.filter-list-divider]="index"> <div class="filters-operation max-w-8% flex-full">
<div class="filters-operation max-w-8% flex-full"> @if ($index) {
@if ($index) { <div class="filters-operation-container">
<div class="filters-operation-container"> <span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
<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>
<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> </div>
@if (index) {
<mat-divider></mat-divider>
}
</div> </div>
@if (index) { }
<mat-divider></mat-divider> </div>
} </section>
</div> } @else {
} <span class="no-data-found tb-prompt flex items-center justify-center" [class.required]="!disabled" translate>alarm-rule.no-filter</span>
@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>
<button mat-button mat-stroked-button color="primary" <button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addFilter()" (click)="addFilter()"
type="button"> type="button">
{{ 'alarm-rule.add-filter' | translate }} {{ '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, AlarmRuleFilterDialogComponent,
AlarmRuleFilterDialogData AlarmRuleFilterDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component"; } 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 { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@ -71,6 +75,10 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
filters: this.fb.array([]) filters: this.fb.array([])
}); });
disabled = false;
areFilterAndPredicateArgumentsValid = areFilterAndPredicateArgumentsValid;
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = complexOperationTranslationMap;
FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap 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 { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) { if (isDisabled) {
this.filterListFormGroup.disable({emitEvent: false}); this.filterListFormGroup.disable({emitEvent: false});
} else { } else {
@ -206,5 +185,4 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
const filters = this.filterListFormGroup.value.filters as Array<AlarmRuleFilter>; const filters = this.filterListFormGroup.value.filters as Array<AlarmRuleFilter>;
this.propagateChange(filters); 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"> <section class="flex flex-col gap-4" [formGroup]="filterListFormGroup">
<div class="tb-form-panel no-padding no-gap stroked"> @if (predicatesFormArray.length) {
<div class="flex flex-row"> <div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-full flex-row items-center justify-start gap-2"> <div class="flex flex-row">
<div class="flex flex-1 flex-row gap-4 pl-2"> <div class="flex flex-full flex-row items-center justify-start gap-2">
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label> <div class="flex flex-1 flex-row gap-4 pl-2">
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label> <label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label>
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</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> </div>
<span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div> </div>
</div> <mat-divider style="padding-bottom: 5px;"></mat-divider>
<mat-divider style="padding-bottom: 5px;"></mat-divider> <div class="predicate-list">
<div class="predicate-list"> @for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) { <div class="flex flex-row items-stretch justify-start relative"
<div class="flex flex-row items-stretch justify-start relative" [class.key-filter-list-divider]="$index">
[class.key-filter-list-divider]="$index"> @if (index) {
@if (index) { <div class="filters-operation-container">
<div class="filters-operation-container"> <span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span> </div>
</div> }
} <div class="flex flex-full flex-col">
<div class="flex flex-full flex-col"> <div class="flex flex-1 flex-row items-center justify-start">
<div class="flex flex-1 flex-row items-center justify-start"> <tb-alarm-rule-filter-predicate
<tb-alarm-rule-filter-predicate class="flex-1"
class="flex-1" [arguments]="arguments"
[arguments]="arguments" [argumentInUse]="argumentInUse"
[argumentInUse]="argumentInUse" [valueType]="valueType"
[valueType]="valueType" [formControl]="predicateControl">
[formControl]="predicateControl"> </tb-alarm-rule-filter-predicate>
</tb-alarm-rule-filter-predicate> <button mat-icon-button color="primary"
<button mat-icon-button color="primary" [disabled]="disabled"
[disabled]="disabled" type="button"
type="button" (click)="removePredicate($index)"
(click)="removePredicate($index)" matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltip="{{ 'filter.remove-filter' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <mat-icon>close</mat-icon>
<mat-icon>close</mat-icon> </button>
</button> </div>
</div> </div>
</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> } @else {
<div class="flex flex-row gap-2"> <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" <button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)" (click)="addPredicate(false)"
type="button"> type="button">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
<button mat-button <button mat-button
color="primary" color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)" (click)="addPredicate(true)"
type="button"> type="button">
{{ 'filter.add-complex' | translate }} {{ '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 { writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void {
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) { if (isDefinedAndNotNull(predicateValue?.duration?.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse); const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) { if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) {
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 { writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
if (isDefinedAndNotNull(predicateValue.dynamicValueArgument)) { if (isDefinedAndNotNull(predicateValue?.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse); const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.dynamicValueArgument)) { if (!availableArgument.includes(predicateValue.dynamicValueArgument)) {
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> </div>
} }
} }
@if (filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA) { <tb-alarm-rule-filter-predicate-no-data-value class="flex-full" [class.!hidden]="!(filterPredicateFormGroup.get('operation').value === stringOperation.NO_DATA)"
<tb-alarm-rule-filter-predicate-no-data-value class="flex-full" [arguments]="arguments"
[arguments]="arguments" [argumentInUse]="argumentInUse"
[argumentInUse]="argumentInUse" [valueType]="valueType"
[valueType]="valueType" formControlName="duration">
formControlName="duration"> </tb-alarm-rule-filter-predicate-no-data-value>
</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)"
} @else if (type !== filterPredicateType.COMPLEX) { [arguments]="arguments"
<tb-alarm-rule-filter-predicate-value class="flex-full" [argumentInUse]="argumentInUse"
[arguments]="arguments" [valueType]="valueType"
[argumentInUse]="argumentInUse" formControlName="value">
[valueType]="valueType" </tb-alarm-rule-filter-predicate-value>
formControlName="value">
</tb-alarm-rule-filter-predicate-value>
}
</div> </div>
</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, alarmRuleNumericOperationTranslationMap,
AlarmRuleStringOperation, AlarmRuleStringOperation,
alarmRuleStringOperationTranslationMap, alarmRuleStringOperationTranslationMap,
checkPredicates,
ComplexAlarmRuleFilterPredicate ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
import { MatDialog } from "@angular/material/dialog"; import { MatDialog } from "@angular/material/dialog";
@ -111,6 +112,18 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
this.updateModel(); 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( this.filterPredicateFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => { ).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)); 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 (Array.isArray(predicates)) {
if (!checkPredicates(predicates)) { if (!checkPredicates(predicates, validSet)) {
return false; return false;
} }
} }
@ -172,8 +170,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
} }
if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) { if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) {
this.type = AlarmRuleFilterPredicateType[this.valueType]; 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}); this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false});
} else { } else {
this.filterPredicateFormGroup.get('duration').disable({emitEvent: false});
this.filterPredicateFormGroup.get('value').enable({emitEvent: false});
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false}); this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
} }
} }
@ -183,30 +185,6 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
if (predicate.operation === 'NO_DATA') { if (predicate.operation === 'NO_DATA') {
this.propagateChange(predicate.duration); this.propagateChange(predicate.duration);
} else { } 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}); 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" <div class="tb-filter-text"
tbTruncateWithTooltip tbTruncateWithTooltip
[class.required]="isRequired" [class.required]="isRequired"
[class.disabled]="disabled"
[class.nowrap]="nowrap" [class.nowrap]="nowrap"
[class.wrap]="!nowrap" [class.wrap]="!nowrap"
[innerHTML]="filterText"> [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; color: #f44336;
padding: 0; padding: 0;
} }
&.disabled {
color: rgba(0,0,0,0.38);
}
&.nowrap { &.nowrap {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; 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() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
disabled = false;
private alarmRuleExpressionValue: AlarmRuleExpression; private alarmRuleExpressionValue: AlarmRuleExpression;
get alarmRuleExpression(): AlarmRuleExpression { get alarmRuleExpression(): AlarmRuleExpression {
return this.alarmRuleExpressionValue; 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. limitations under the License.
--> -->
<div class="flex flex-col gap-3"> <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"> <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 <table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear> [matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'"> <ng-container [matColumnDef]="'name'">
@ -94,6 +96,7 @@
<button type="button" <button type="button"
mat-icon-button mat-icon-button
#button #button
[disabled]="disable"
(click)="manageArgument($event, button, argument)" (click)="manageArgument($event, button, argument)"
[matTooltip]="'action.edit' | translate" [matTooltip]="'action.edit' | translate"
matTooltipPosition="above"> matTooltipPosition="above">
@ -119,14 +122,15 @@
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayColumns"></mat-header-row> <mat-header-row class="mat-row-select" *matHeaderRowDef="displayColumns"></mat-header-row>
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row> <mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table> </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)) { @if (errorText || (dataSource.isEmpty() | async)) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/> <tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
} }
</div> </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"> <div class="flex h-9 justify-between">
<button <button
type="button" type="button"
@ -134,7 +138,7 @@
color="primary" color="primary"
#button #button
(click)="manageArgument($event, button)" (click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disabledAddButton" [disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disable"
> >
{{ 'calculated-fields.add-argument' | translate }} {{ 'calculated-fields.add-argument' | translate }}
</button> </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 { import {
AfterViewInit, AfterViewInit,
booleanAttribute,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
@ -87,7 +88,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() entityName: string; @Input() entityName: string;
@Input() ownerId: EntityId; @Input() ownerId: EntityId;
@Input() isScript: boolean; @Input() isScript: boolean;
@Input() disabledAddButton = false; @Input({transform: booleanAttribute}) disable = false;
@Input() watchKeyChange = false; @Input() watchKeyChange = false;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild(MatSort, { static: true }) sort: MatSort;
@ -220,8 +221,6 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling'; this.errorText = 'calculated-fields.hint.arguments-simple-with-rolling';
} else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { } else if (this.argumentsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.arguments-entity-not-found'; this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
} else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty';
} else { } else {
this.errorText = ''; 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. 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"> <mat-toolbar color="primary">
<h2>{{ 'entity.type-calculated-field' | translate}}</h2> <h2>{{ 'entity.type-calculated-field' | translate}}</h2>
<span class="flex-1"></span> <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="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 <table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear> [matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'"> <ng-container [matColumnDef]="'name'">
@ -121,14 +123,14 @@
*matHeaderRowDef="['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-header-row> *matHeaderRowDef="['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-header-row>
<mat-row *matRowDef="let argument; columns: ['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-row> <mat-row *matRowDef="let argument; columns: ['name', 'entityType', 'target', 'key', 'reportStrategy', 'actions']"></mat-row>
</table> </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) { @if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/> <tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
} }
</div> </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"> <div class="flex h-9 justify-between">
<button type="button" <button type="button"
mat-stroked-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 { private updateErrorText(): void {
if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) {
this.errorText = 'calculated-fields.hint.geofencing-entity-not-found'; this.errorText = 'calculated-fields.hint.geofencing-entity-not-found';
} else if (!this.zoneGroupsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.geofencing-empty';
} else { } else {
this.errorText = ''; 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="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 <table mat-table [dataSource]="dataSource" class="overflow-hidden bg-transparent" matSort
[matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear> [matSortActive]="sortOrder.property" [matSortDirection]="sortOrder.direction" matSortDisableClear>
<ng-container [matColumnDef]="'name'"> <ng-container [matColumnDef]="'name'">
@ -98,13 +100,10 @@
<mat-header-row class="mat-row-select" *matHeaderRowDef=displayColumns></mat-header-row> <mat-header-row class="mat-row-select" *matHeaderRowDef=displayColumns></mat-header-row>
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row> <mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table> </table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false" </div>
class="tb-prompt flex flex-1 items-end justify-center"> <div [class.!hidden]="(dataSource.isEmpty() | async) === false"
{{ 'calculated-fields.metrics.no-metrics-configured' | translate }} class="tb-prompt flex flex-1 items-end justify-center required">
</div> {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
@if (errorText) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div> </div>
<div class="flex h-9 justify-between"> <div class="flex h-9 justify-between">
<button type="button" <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> </tb-time-unit-input>
</div> </div>
<tb-calculate-field-output formControlName="output" [entityId]="entityId" simpleMode hiddenName> <tb-calculate-field-output formControlName="output" [entityId]="entityId" simpleMode hiddenName>
<div class="tb-form-row simpleMode flex-1"> @if (relatedAggregationConfiguration.get('output').value?.type === OutputType.Timeseries) {
<mat-slide-toggle class="mat-slide" formControlName="useLatestTs"> <div class="tb-form-row simpleMode flex-1">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}"> <mat-slide-toggle class="mat-slide" formControlName="useLatestTs">
<div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div> <div tb-hint-tooltip-icon="{{ 'calculated-fields.hint.use-latest-timestamp' | translate }}">
</div> <div translate tbTruncateWithTooltip>calculated-fields.use-latest-timestamp</div>
</mat-slide-toggle> </div>
</div> </mat-slide-toggle>
</div>
}
</tb-calculate-field-output> </tb-calculate-field-output>
</div> </div>

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

@ -23,6 +23,7 @@
[tenantId]="tenantId" [tenantId]="tenantId"
[ownerId]="ownerId" [ownerId]="ownerId"
[entityName]="entityName" [entityName]="entityName"
[watchKeyChange]="true"
[isScript]="isScript" /> [isScript]="isScript" />
</div> </div>
<div class="tb-form-panel no-gap"> <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>
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.type' | translate }} </mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 33%"> {{ 'relation.type' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let relation"> <mat-cell *matCellDef="let relation" class="type">
{{ relation.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> </mat-cell>
</ng-container> </ng-container>
<ng-container *ngIf="direction === directions.FROM" matColumnDef="toEntityTypeName"> <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; overflow: hidden;
text-overflow: ellipsis; 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 { get showEntityLink(): boolean {
return this.selectEntityFormGroup.get('entity').value && this.disabled && this.entityURL !== ''; 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 ''; 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> { public openCalculatedFieldImportDialog(importTitle = 'calculated-fields.import', importFileLabel = 'calculated-fields.file'): Observable<CalculatedField> {
return this.openImportDialog('calculated-fields.import', 'calculated-fields.file').pipe( return this.openImportDialog(importTitle, importFileLabel).pipe(
catchError(() => of(null)), 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 { CustomTimeSchedulerItem } from "@shared/models/device.models";
import { DashboardId } from "@shared/models/id/dashboard-id"; import { DashboardId } from "@shared/models/id/dashboard-id";
import { TimeUnit } from "@shared/models/time/time.models"; import { TimeUnit } from "@shared/models/time/time.models";
import { import { ComplexOperation, EntityKeyValueType, FilterPredicateType } from "@shared/models/query/query.models";
BooleanOperation,
ComplexOperation,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
StringOperation
} from "@shared/models/query/query.models";
import { EntityType } from "@shared/models/entity-type.models"; import { EntityType } from "@shared/models/entity-type.models";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { CalculatedField } from "@shared/models/calculated-field.models"; import { CalculatedField, CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export enum AlarmRuleScheduleType { export enum AlarmRuleScheduleType {
ANY_TIME = 'ANY_TIME', ANY_TIME = 'ANY_TIME',
@ -250,3 +243,62 @@ export const alarmRuleDefaultScript =
'return temperature > 20;' 'return temperature > 20;'
export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, expression: string, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>; 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 ```javascript
{ {
"type": "CUSTOM" "type": "CUSTOM",
"timezone": "Europe/Kiev", "timezone": "Europe/Kiev",
"items": [ "items": [
{ {

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

@ -1122,7 +1122,7 @@
"add-argument": "Add argument", "add-argument": "Add argument",
"test-script-function": "Test script function", "test-script-function": "Test script function",
"test-expression-function": "Test expression 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-settings": "Argument settings",
"argument-current": "Current entity", "argument-current": "Current entity",
"argument-current-tenant": "Current tenant", "argument-current-tenant": "Current tenant",
@ -1176,7 +1176,7 @@
"target-zone": "Target zone", "target-zone": "Target zone",
"perimeter-key": "Perimeter key", "perimeter-key": "Perimeter key",
"report-strategy": "Report strategy", "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.", "no-zone-configured-required": "At least one zone group must be configured.",
"add-zone-group": "Add zone group", "add-zone-group": "Add zone group",
"report-transition-event-only": "Transition events only", "report-transition-event-only": "Transition events only",
@ -1245,7 +1245,7 @@
"key": "Key", "key": "Key",
"function": "Function" "function": "Function"
}, },
"no-metrics-configured": "No metrics configured", "no-metrics-configured": "At least one metric is required.",
"add-metric": "Add metric", "add-metric": "Add metric",
"max-metrics": "Maximum number of metrics reached.", "max-metrics": "Maximum number of metrics reached.",
"metric-settings": "Metric settings", "metric-settings": "Metric settings",
@ -1268,7 +1268,7 @@
"ttl-min": "Only 0 minimum TTL is allowed", "ttl-min": "Only 0 minimum TTL is allowed",
"processing-parameters": "Processing parameters", "processing-parameters": "Processing parameters",
"hint": { "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", "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": "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.", "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-old": "Old",
"alarm-rules-actual": "Actual", "alarm-rules-actual": "Actual",
"severities": "Severities", "severities": "Severities",
"cleared": "Clearing condition", "cleared": "Clear condition",
"delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?", "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-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} }?", "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} }", "list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }",
"selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected", "selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected",
"import": "Import alarm rule", "import": "Import alarm rule",
"file": "Alarm rule file",
"export": "Export alarm rule", "export": "Export alarm rule",
"export-failed-error": "Unable to export alarm rule: {{error}}", "export-failed-error": "Unable to export alarm rule: {{error}}",
"entity-type": "Entity type", "entity-type": "Entity type",
@ -1400,6 +1401,7 @@
"target-entity": "Target entity", "target-entity": "Target entity",
"target-entities": "Target entities", "target-entities": "Target entities",
"alarm-type": "Alarm type", "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-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.", "alarm-type-pattern": "Alarm type is invalid.",
"alarm-type-max-length": "Alarm type should be less than 256 characters.", "alarm-type-max-length": "Alarm type should be less than 256 characters.",
@ -1435,8 +1437,7 @@
"add-filter": "Add argument filter", "add-filter": "Add argument filter",
"edit-filter": "Argument filter", "edit-filter": "Argument filter",
"remove-filter": "Remove argument filter", "remove-filter": "Remove argument filter",
"no-filter": "No argument filters configured", "no-filter": "At least one filter is required.",
"filter-required": "At least one filter must be configured.",
"conditions": { "conditions": {
"simple": "Simple", "simple": "Simple",
"duration": "Duration", "duration": "Duration",
@ -1495,9 +1496,9 @@
"condition-type": "Condition type", "condition-type": "Condition type",
"condition-type-hint": "\"Duration\" and \"Repeating\" options are not available when the \"Missing for\" operation is used in the filter.", "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", "select-alarm-severity": "Select alarm severity",
"add-create-alarm-rule-prompt": "At least one creation condition should be configured", "add-create-alarm-rule-prompt": "At least one trigger condition is required.",
"add-create-alarm-rule": "Add creation condition", "add-create-alarm-rule": "Add trigger condition",
"add-clear-alarm-rule": "Add clearing condition", "add-clear-alarm-rule": "Add clear condition",
"condition-duration": "Condition duration", "condition-duration": "Condition duration",
"condition-duration-value": "Duration value", "condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit", "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-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-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.", "condition-repeating-value-required": "Count of events is required.",
"create-conditions": "Creation conditions", "create-conditions": "Alarm Trigger Conditions",
"clear-condition": "Clearing condition", "clear-condition": "Alarm Clear Condition",
"no-clear-alarm-rule": "Clearing condition not configured", "no-clear-alarm-rule": "No clear condition configured.",
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities", "propagate-alarm": "Propagate alarm to related entities",
"alarm-rule-relation-types-list": "Relation types", "alarm-rule-relation-types-list": "Relation types",
@ -4981,7 +4982,8 @@
"additional-info": "Additional info (JSON)", "additional-info": "Additional info (JSON)",
"invalid-additional-info": "Unable to parse additional info json.", "invalid-additional-info": "Unable to parse additional info json.",
"no-relations-text": "No relations found", "no-relations-text": "No relations found",
"not": "Not" "not": "Not",
"copy-type": "Copy type"
}, },
"resource": { "resource": {
"add": "Add resource", "add": "Add resource",

Loading…
Cancel
Save