Browse Source

Merge pull request #14463 from ArtemDzhereleiko/AD/bug-fix/add-validation

Bug-fixes and add validation for alarm rules
pull/14406/head
Vladyslav Prykhodko 8 months ago
committed by GitHub
parent
commit
4e2b44e4ce
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      ui-ngx/src/app/core/services/menu.models.ts
  2. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  3. 48
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  4. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html
  5. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  6. 47
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  7. 129
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  8. 11
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  9. 74
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  10. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  11. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
  12. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
  13. 33
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  14. 13
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  15. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  16. 51
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  17. 32
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  18. 13
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
  19. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  20. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  21. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
  22. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.ts
  23. 9
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  25. 36
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  26. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  27. 3
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
  28. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  29. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/entity-aggregation-arguments-table.component.ts
  30. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  31. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts
  32. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html
  33. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  34. 4
      ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html
  35. 11
      ui-ngx/src/app/modules/home/components/vc/entity-types-version.component.scss
  36. 2
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  37. 7
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  38. 2
      ui-ngx/src/app/shared/models/vc.models.ts
  39. 50
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
ui-ngx/src/app/core/services/menu.models.ts

@ -510,7 +510,7 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
MenuId.alarms,
{
id: MenuId.alarms,
name: 'alarm.alarms',
name: 'alarm.alarm-list',
type: 'link',
path: '/alarms/alarms',
icon: 'mdi:alert-outline'

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

@ -27,8 +27,10 @@
<mat-form-field class="mat-block" appearance="outline">
<textarea matInput [formControl]="alarmDetailsControl" rows="5"
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field>
<div class="tb-form-hint tb-primary-fill flex items-center gap-2">
<span [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></span>
</div>
</div>
</div>
<div mat-dialog-actions class="justify-end">

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

@ -130,30 +130,32 @@
{{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
<div class="tb-form-panel stroked no-padding no-gap">
<div class="tb-form-row no-border">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</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>
}
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block" 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>
}
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }}

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

@ -66,7 +66,7 @@
</tb-entity-subtype-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.entity-type</div>
<div class="fixed-title-width" translate>alarm-rule.target-entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
@ -78,7 +78,7 @@
</div>
@if (alarmRuleFilterConfigForm.get('entityType').value) {
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.alarm-rule-entity-list</div>
<div class="fixed-title-width" translate>alarm-rule.target-entities</div>
<tb-entity-list appearance="outline"
subscriptSizing="dynamic"
class="flex flex-1"

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

@ -138,16 +138,16 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
if (this.pageMode) {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.entity-type', '15%',
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.target-entity-type', '15%',
entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)));
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.entity-name', '30%',
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.target-entity', '30%',
entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']),
entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
}
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '70px',
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '90px',
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false));
this.cellActionDescriptors.push(

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

@ -90,6 +90,34 @@
</div>
}
</div>
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="true">
<mat-expansion-panel-header>
{{ 'alarm-rule.filter-preview' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div>
@if (specText) {
<span class="spec-text">{{ specText }}</span>
}
@if (conditionFormGroup.get('expression.filters').value?.length) {
<tb-alarm-rule-filter-text [alarmRuleExpression]="conditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="false"
required>
</tb-alarm-rule-filter-text>
} @else {
<span class="tb-prompt justify-center">{{ 'alarm-rule.no-filter-preview' | translate }}</span>
}
</div>
</ng-template>
</mat-expansion-panel>
</div>
}
<section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
@ -150,7 +178,7 @@
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container>
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div>
</div>
</div>
@ -166,9 +194,8 @@
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty">
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty || !filtersValid">
{{ 'action.save' | translate }}
</button>
}
@ -190,6 +217,9 @@
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) {
<mat-error>{{ defaultValuePatternError | translate }}</mat-error>
}
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field>
</div>
</ng-template>
@ -202,9 +232,14 @@
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')">
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
@if (conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')) {
<mat-error>
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
}
@if (type === AlarmConditionType.REPEATING) {
<mat-hint translate>alarm-rule.condition-repeating-value-hint</mat-hint>
}
</mat-form-field>
</ng-container>
</ng-template>

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

@ -24,14 +24,6 @@ import { DialogComponent } from '@app/shared/components/dialog.component';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType,
AlarmRuleFilter
} from "@shared/models/alarm-rule.models";
import {
CalculatedFieldArgument,
getCalculatedFieldArgumentsEditorCompleter,
@ -39,8 +31,18 @@ import {
} from "@shared/models/calculated-field.models";
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { ComplexOperation } from "@shared/models/query/query.models";
import { Observable } from "rxjs";
import { TranslateService } from "@ngx-translate/core";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType,
AlarmRuleFilter,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
@ -97,7 +99,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
complexOperationTranslationMap = filterOperationTranslationMap;
specText = '';
filtersValid: boolean = false;
functionArgs: Array<string>;
argumentsEditorCompleter: TbEditorCompleter;
@ -112,7 +118,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: FormBuilder) {
private fb: FormBuilder,
private translate: TranslateService) {
super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
@ -131,28 +138,37 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
staticValue: this.condition?.value?.staticValue,
dynamicValueArgument: this.condition?.value?.dynamicValueArgument,
dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.value?.dynamicValueArgument) ? this.condition?.value?.dynamicValueArgument : null,
},
count: {
staticValue: this.condition?.count?.staticValue ?? null,
dynamicValueArgument: this.condition?.count?.dynamicValueArgument
dynamicValueArgument: Object.keys(this.data.arguments).includes(this.condition?.count?.dynamicValueArgument) ? this.condition?.count?.dynamicValueArgument : null
}
}, {emitEvent: false});
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.checkIsNoData(this.condition?.expression?.filters);
this.conditionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type) => {
this.updateValidators(type, true);
this.updateValidators(type);
});
this.conditionFormGroup.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((value) => {
this.updateSpecText(value.type);
})
this.conditionFormGroup.get('expression.filters').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((filters) => {
this.filtersValid = this.areFilterAndPredicateArgumentsValid(filters, this.argumentsList);
this.checkIsNoData(filters);
});
@ -175,6 +191,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
});
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateSpecText(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
}
@ -189,6 +206,39 @@ 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});
@ -218,7 +268,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
return search(data);
};
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) {
private updateValidators(type: AlarmRuleConditionType) {
switch (type) {
case AlarmRuleConditionType.DURATION:
this.conditionFormGroup.get('unit').enable({emitEvent: false});
@ -248,6 +298,55 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}
}
private updateSpecText(type: AlarmRuleConditionType) {
this.specText = '';
const value = this.conditionFormGroup.get('value').value;
const count = this.conditionFormGroup.get('count').value;
switch (type) {
case AlarmRuleConditionType.SIMPLE:
break;
case AlarmRuleConditionType.DURATION:
let duringText = '';
switch (this.conditionFormGroup.get('unit').value) {
case TimeUnit.SECONDS:
duringText = this.translate.instant('timewindow.seconds', {seconds: value.staticValue});
break;
case TimeUnit.MINUTES:
duringText = this.translate.instant('timewindow.minutes', {minutes: value.staticValue});
break;
case TimeUnit.HOURS:
duringText = this.translate.instant('timewindow.hours', {hours: value.staticValue});
break;
case TimeUnit.DAYS:
duringText = this.translate.instant('timewindow.days', {days: value.staticValue});
break;
}
if (value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${value.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText
});
}
break;
case AlarmRuleConditionType.REPEATING:
if (count.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', {
attribute: `${count.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times',
{count: count.staticValue});
}
break;
}
if (this.specText.length > 0) {
this.specText = this.specText + ':';
}
}
cancel(): void {
this.dialogRef.close(null);
}

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

@ -21,7 +21,7 @@
<button [disabled]="disabled"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() ? 'primary' : 'warn'"
mat-stroked-button [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'"
(click)="openFilterDialog($event)">
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
@ -32,20 +32,23 @@
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() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
<mat-icon [color]="conditionSet() && this.filtersArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">
{{ conditionSet() ? 'edit' : 'add' }}
</mat-icon>
</div>
</button>
</div>
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div>
<button [disabled]="disabled"
[color]="schedulerArgumentsValid ? 'primary' : 'warn'"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button color="primary"
mat-stroked-button
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
<mat-icon [color]="schedulerArgumentsValid ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
</div>
</button>
</div>

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

@ -14,8 +14,9 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
@ -68,7 +69,7 @@ import { Observable } from "rxjs";
}
]
})
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator {
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator, OnChanges {
@Input()
@coerceBoolean()
@ -96,11 +97,15 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
specText = '';
filtersArgumentsValid: boolean = true;
schedulerArgumentsValid: boolean = true;
scheduleText = '';
private modelValue: AlarmRuleCondition;
private propagateChange = (v: any) => { };
private onValidatorChange = () => { };
constructor(private dialog: MatDialog,
private fb: FormBuilder,
@ -115,6 +120,10 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
registerOnTouched(fn: any): void {
}
registerOnValidatorChange(fn: () => void): void {
this.onValidatorChange = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -127,14 +136,68 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
writeValue(value: AlarmRuleCondition): void {
this.modelValue = value;
this.updateConditionInfo();
if (value) {
this.onValidatorChange();
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.arguments) {
if (changes.arguments && !changes.arguments.firstChange && this.modelValue) {
this.onValidatorChange();
}
}
}
private isScheduleArgumentValid(obj: any, validArguments: string[]): boolean {
const arg = obj?.schedule?.dynamicValueArgument;
return !arg || validArguments.includes(arg);
}
private areFilterAndPredicateArgumentsValid(obj: any, validArguments: string[]): boolean {
const validSet = new Set(validArguments);
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)) {
return false;
}
}
}
return true;
}
public conditionSet() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
}
public validate(): ValidationErrors | null {
return this.conditionSet() ? null : {
public validate(control: AbstractControl): ValidationErrors | null {
this.filtersArgumentsValid = this.areFilterAndPredicateArgumentsValid(this.modelValue, Object.keys(this.arguments));
this.schedulerArgumentsValid = this.isScheduleArgumentValid(this.modelValue, Object.keys(this.arguments));
this.onValidatorChange = () => {
control.updateValueAndValidity({ emitEvent: true });
};
return this.conditionSet() && this.filtersArgumentsValid && this.schedulerArgumentsValid ? null : {
alarmRuleCondition: {
valid: false,
}
@ -226,6 +289,9 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
private updateModel() {
this.updateConditionInfo();
this.propagateChange(this.modelValue);
if (this.modelValue) {
this.onValidatorChange();
}
}
public openScheduleDialog($event: Event) {

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

@ -20,11 +20,11 @@
</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" 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
</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="alarmDetails" placeholder="{{ 'alarm-rule.alarm-rule-additional-info' | translate }}">
<input matInput formControlName="alarmDetails" placeholder="{{ 'action.set' | translate }}">
<button type="button"
matSuffix mat-icon-button aria-label="Open in new"
(click)="openEditDetailsDialog($event)">
@ -42,7 +42,7 @@
subscriptSizing="dynamic"
inlineField
class="flex-1"
placeholder="{{ 'alarm-rule.alarm-rule-mobile-dashboard' | translate }}"
placeholder="{{ 'action.set' | translate }}"
formControlName="dashboardId">
</tb-dashboard-autocomplete>
</div>

5
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss

@ -20,6 +20,11 @@
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
}
.spec-text {
font-size: 14px;
color: rgba(0, 0, 0, 0.54);
}
}
.tbel-script-lang-chip {
line-height: 20px;

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts

@ -171,7 +171,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (value) {
this.modelValue = value;
if (this.modelValue.dynamicValueArgument) {
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false});
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(Object.keys(this.arguments).includes(this.modelValue.dynamicValueArgument) ? this.modelValue.dynamicValueArgument : null, {emitEvent: false});
} else {
switch (this.modelValue.staticValue.type) {
case AlarmRuleScheduleType.SPECIFIC_TIME:

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

@ -25,20 +25,25 @@
</button>
</mat-toolbar>
<div mat-dialog-content>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
<section class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ data.valueType === EntityKeyValueType.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filters' | translate }}
</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="complexOperationEnum.AND">{{ complexOperationTranslations.get(complexOperationEnum.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="complexOperationEnum.OR">{{ complexOperationTranslations.get(complexOperationEnum.OR) | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[argumentInUse]="data.argumentInUse"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"

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

@ -21,15 +21,12 @@ import { AppState } from '@core/core.state';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType
} from '@shared/models/query/query.models';
import { ComplexOperation, EntityKeyValueType } from '@shared/models/query/query.models';
import {
AlarmRuleFilterPredicate,
AlarmRuleFilterPredicateType,
ComplexAlarmRuleFilterPredicate
ComplexAlarmRuleFilterPredicate,
filterOperationTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@ -58,9 +55,9 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
}
);
complexOperations = Object.keys(ComplexOperation);
EntityKeyValueType = EntityKeyValueType;
complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap;
complexOperationTranslations = filterOperationTranslationMap;
isAdd: boolean;

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

@ -69,7 +69,7 @@
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ filterFormGroup.get('valueType').value === entityKeyValueTypeEnum.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filter' | translate }}
{{ 'alarm-rule.filters' | translate }}
</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
@ -95,7 +95,7 @@
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty">
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty || !predicatesValid">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>

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

@ -21,18 +21,18 @@
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap
} from '@shared/models/query/query.models';
import { ComplexOperation, EntityKeyValueType, entityKeyValueTypesMap } from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import {
AlarmRuleFilter,
AlarmRuleFilterPredicate,
filterOperationTranslationMap
} 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;
@ -57,7 +57,9 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
entityKeyValueTypes = entityKeyValueTypesMap;
complexOperationTranslationMap = complexOperationTranslationMap;
complexOperationTranslationMap = filterOperationTranslationMap;
predicatesValid: boolean = false;
ComplexOperation = ComplexOperation;
@ -84,6 +86,14 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
operation: [this.data.filter.operation ?? ComplexOperation.AND]
}
);
this.predicatesValid = this.isPredicateArgumentsValid(this.data.filter.predicates);
this.filterFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicatesValid = this.isPredicateArgumentsValid(predicates);
});
this.filterFormGroup.get('valueType').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((valueType: EntityKeyValueType) => {
@ -106,6 +116,31 @@ 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);
}

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

@ -39,19 +39,26 @@
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1">{{ filterControl.value?.argument }}</div>
<div class="flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.valueType) | translate }}</div>
<button mat-icon-button color="primary"
<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="{{ 'filter.edit-key-filter' | translate }}"
matTooltip="{{ 'alarm-rule.edit-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>{{'edit'}}</mat-icon>
<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="{{ 'filter.remove-key-filter' | translate }}"
matTooltip="{{ 'alarm-rule.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
@ -62,14 +69,15 @@
}
</div>
}
<span [class.!hidden]="!!filtersFormArray.length"
class="no-data-found flex items-center justify-center"
translate>
filter.no-key-filters
</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-raised-button color="primary"
<button mat-button mat-stroked-button color="primary"
(click)="addFilter()"
type="button">
{{ 'alarm-rule.add-filter' | translate }}

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

@ -30,6 +30,10 @@
&-divider {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.filters-text {
font-size: 14px;
}
}
.filters-operation {
display: flex;
@ -39,7 +43,8 @@
background-color: white;
}
&-label {
font-weight: 500;
font-size: 15px;
font-weight: 400;
color: #00695C;
padding: 0 8px;
border-radius: 4px;
@ -47,4 +52,10 @@
background-color: rgba(#00695C, 0.04);
}
}
.tb-budge-button {
--mat-badge-legacy-small-size-container-size: 8px;
--mat-badge-small-size-container-overlap-offset: -5px;
--mat-badge-small-size-text-size: 0;
}
}

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

@ -102,6 +102,36 @@ 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 {
if (isDisabled) {
this.filterListFormGroup.disable({emitEvent: false});

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

@ -58,21 +58,23 @@
</div>
</div>
}
<span [class.!hidden]="!!predicatesFormArray.length"
[class.disabled]="disabled"
class="no-data-found flex items-center justify-center" translate>
filter.no-filters
</span>
@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 class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary"
<button mat-button mat-stroked-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
<button mat-button
color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button">

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

@ -39,7 +39,8 @@
background-color: white;
}
&-label {
font-weight: 500;
font-size: 15px;
font-weight: 400;
color: #00695C;
padding: 0 8px;
border-radius: 4px;

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

@ -28,6 +28,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { TimeUnit, timeUnitTranslations } from "@home/components/rule-node/rule-node-config.models";
import { AlarmRuleFilterPredicateType, NoDataAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { isDefinedAndNotNull } from "@core/utils";
@Component({
selector: 'tb-alarm-rule-filter-predicate-no-data-value',
@ -149,8 +150,14 @@ export class AlarmRuleFilterPredicateNoDataValueComponent implements ControlValu
}
writeValue(predicateValue: NoDataAlarmRuleFilterPredicate): void {
if (isDefinedAndNotNull(predicateValue.duration.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.duration.dynamicValueArgument)) {
predicateValue.duration.dynamicValueArgument = '';
}
this.dynamicModeControl.patchValue(true, {emitEvent: false});
}
this.filterPredicateValueNoDataFormGroup.patchValue(predicateValue, {emitEvent: false});
this.dynamicModeControl.patchValue(!!predicateValue?.duration?.dynamicValueArgument?.length, {emitEvent: false});
}
private updateModel() {

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

@ -31,6 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { isDefinedAndNotNull } from "@core/utils";
@Component({
selector: 'tb-alarm-rule-filter-predicate-value',
@ -165,8 +166,14 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
if (isDefinedAndNotNull(predicateValue.dynamicValueArgument)) {
const availableArgument = this.argumentsList.filter(arg => arg !== this.argumentInUse);
if (!availableArgument.includes(predicateValue.dynamicValueArgument)) {
predicateValue.dynamicValueArgument = '';
}
this.dynamicModeControl.patchValue(true, {emitEvent: false});
}
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
this.dynamicModeControl.patchValue(!!predicateValue?.dynamicValueArgument?.length, {emitEvent: false});
}
private updateModel() {

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

@ -60,7 +60,7 @@
<div class="tb-form-row no-border no-padding flex-1">
<button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;"
class="block w-full"
mat-stroked-button color="primary"
mat-stroked-button [color]="predicateValid ? 'primary' : 'warn'"
(click)="openComplexFilterDialog()">
<div class="flex items-center gap-2">
<span class="w-full text-start" translate>filter.complex-filter</span>

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

@ -98,6 +98,8 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
booleanOperationEnum = AlarmRuleBooleanOperation;
booleanOperationTranslations = alarmRuleBooleanOperationTranslationMap;
predicateValid: boolean = false;
private propagateChange= (v: any) => { };
constructor(private fb: FormBuilder,
@ -108,6 +110,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
).subscribe(() => {
this.updateModel();
});
this.filterPredicateFormGroup.get('predicates').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(predicates => {
this.predicateValid = this.isPredicateArgumentsValid(predicates);
})
}
registerOnChange(fn: any): void {
@ -132,8 +140,36 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
}
}
private isPredicateArgumentsValid(predicates: any): 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)) {
return false;
}
}
return true;
}
writeValue(predicate: AlarmRuleFilterPredicate): void {
this.type = predicate.type;
if ((predicate as ComplexAlarmRuleFilterPredicate)?.predicates) {
this.predicateValid = this.isPredicateArgumentsValid((predicate as ComplexAlarmRuleFilterPredicate)?.predicates);
}
if (predicate.type === AlarmRuleFilterPredicateType.NO_DATA) {
this.type = AlarmRuleFilterPredicateType[this.valueType];
this.filterPredicateFormGroup.patchValue({operation: 'NO_DATA', duration: predicate}, {emitEvent: false});

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

@ -19,5 +19,6 @@
tbTruncateWithTooltip
[class.required]="isRequired"
[class.nowrap]="nowrap"
[class.wrap]="!nowrap"
[innerHTML]="filterText">
</div>

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

@ -28,6 +28,9 @@
text-overflow: ellipsis;
overflow: hidden;
}
&.wrap {
white-space: pre-wrap !important;
}
}
}

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

@ -98,7 +98,7 @@ export class AlarmRuleFilterTextComponent {
private updateFilterText(value: AlarmRuleExpression) {
this.isRequired = false;
if (value && (value.expression || value.filters)) {
if (value && (value.expression || value.filters?.length)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
} else {

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

@ -64,6 +64,7 @@ export class EntityAggregationArgumentsTableComponent extends CalculatedFieldArg
hint: 'calculated-fields.entity-aggregation.argument-setting-hint',
hiddenDefaultValue: true,
hiddenEntityKeyTypes: true,
watchKeyChange: true,
};
this.isScript = false;

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

@ -123,7 +123,7 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa
this.errorText = 'calculated-fields.hint.arguments-entity-not-found';
} else if (!this.argumentsFormArray.controls.length) {
this.errorText = 'calculated-fields.hint.arguments-empty';
} if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) {
} else if (this.isScript && !this.argumentsFormArray.controls.some(control => isUndefinedOrNull(control.value?.refEntityId) && isUndefinedOrNull(control.value.refDynamicSourceConfiguration))) {
this.errorText = 'calculated-fields.hint.arguments-propagate-argument-must-current-entity';
} else {
this.errorText = '';

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

@ -87,7 +87,8 @@ export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldAr
hiddenEntityTypes: true,
defaultValueRequired: true,
argumentEntityTypes: [ArgumentEntityType.Current],
hint: 'calculated-fields.hint.setting-arguments-aggregation'
hint: 'calculated-fields.hint.setting-arguments-aggregation',
watchKeyChange: true,
};
this.isScript = false;

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html

@ -59,7 +59,7 @@
</div>
<div class="tb-form-panel no-gap" [class.!hidden]="!this.propagateConfiguration.get('applyExpressionToResolvedArguments').value">
<div class="tb-form-panel-title tb-required">
{{ 'calculated-fields.expression' | translate }}
{{ 'calculated-fields.script' | translate }}
</div>
<div>
<tb-js-func required

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -145,7 +145,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
const formValue: any = deepClone(value);
if (this.isScript) {
formValue.expressionSCRIPT = formValue.expression ?? calculatedFieldDefaultScript;
} else {
} else if (value.type === CalculatedFieldType.SIMPLE) {
formValue.expressionSIMPLE = formValue.expression;
}
this.simpleConfiguration.patchValue(formValue, {emitEvent: false});

4
ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html

@ -74,8 +74,8 @@
<mat-checkbox [class.hidden]="entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
<mat-checkbox [class.hidden]="!typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields" class="checkbox-pre-line">
{{ (entityTypeFormGroup.get('entityType').value === entityTypes.CUSTOMER ? 'version-control.export-alarm-rules' : 'version-control.export-calculated-fields') | translate }}
</mat-checkbox>
</div>
</div>

11
ui-ngx/src/app/modules/home/components/vc/entity-types-version.component.scss

@ -56,6 +56,17 @@
}
:host ::ng-deep {
.mat-expansion-panel.entity-type-config {
.entity-type-config-content {
.checkbox-pre-line {
.mdc-form-field {
.mdc-label {
white-space: pre-line;
}
}
}
}
}
.mat-expansion-panel.entity-type-config {
.mat-expansion-panel-body {
padding: 0;

2
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -43,7 +43,7 @@ const routes: Routes = [
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
breadcrumb: {
menuId: MenuId.alarms
menuId: MenuId.alarms_center
}
},
children: [

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

@ -168,6 +168,13 @@ export interface AlarmRuleFilterConfig {
entities?: Array<string>;
}
export const filterOperationTranslationMap = new Map<ComplexOperation, string>(
[
[ComplexOperation.AND, 'alarm-rule.filter-operation.and'],
[ComplexOperation.OR, 'alarm-rule.filter-operation.or'],
]
);
export enum AlarmRuleFilterPredicateType {
STRING = 'STRING',
NUMERIC = 'NUMERIC',

2
ui-ngx/src/app/shared/models/vc.models.ts

@ -265,4 +265,4 @@ export interface EntityDataInfo {
hasCalculatedFields: boolean;
}
export const typesWithCalculatedFields = new Set<EntityType | AliasEntityType>([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]);
export const typesWithCalculatedFields = new Set<EntityType | AliasEntityType>([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE, EntityType.CUSTOMER]);

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

@ -558,6 +558,7 @@
},
"alarm": {
"alarm": "Alarm",
"alarm-list": "Alarm list",
"alarms": "Alarms",
"all-alarms": "All alarms",
"select-alarm": "Select alarm",
@ -1209,8 +1210,9 @@
"propagation-path-related-entities": "Propagation path to related entities",
"propagate-type": {
"arguments-only": "Arguments only",
"expression-result": "Expression result"
"expression-result": "Calculation result"
},
"script": "Script",
"data-propagate": "Data to propagate",
"output-key": "Output key",
"copy-output-key": "Copy output key",
@ -1366,7 +1368,7 @@
"arguments-aggregation": "Defines input parameters used for filtering and aggregation.",
"setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.",
"metrics": "Defines metrics aggregated based on the configured arguments.",
"import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field type."
"import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field structure."
}
},
"alarm-rule": {
@ -1376,7 +1378,7 @@
"alarm-rules-old": "Old",
"alarm-rules-actual": "Actual",
"severities": "Severities",
"cleared": "Clear condition",
"cleared": "Clearing 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} }?",
@ -1393,7 +1395,8 @@
"entity-type": "Entity type",
"target-entity-type": "Target entity type",
"entity-type-required": "Entity type is required.",
"entity-name": "Entity name",
"target-entity": "Target entity",
"target-entities": "Target entities",
"alarm-type": "Alarm type",
"alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
@ -1410,7 +1413,7 @@
"condition-during": "During {{during}}",
"condition-during-dynamic": "During \"{{ attribute }}\"",
"condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }",
"condition-repeat-times-dynamic": "Repeats \"{ attribute }\"",
"condition-repeat-times-dynamic": "Repeats \"{{ attribute }}\"",
"filter-preview": "Filter preview",
"condition-settings": "Condition settings",
"static": "Static",
@ -1419,7 +1422,7 @@
"argument-name": "Argument name",
"value-type": "Value type",
"general": "General",
"filter": "Filter",
"filters": "Filters",
"date-time-hint": "The argument must be in epoch milliseconds. Example: 1698839340000 equals 2023-11-01 12:49:00 UTC.",
"operation": "Operation",
"value-source": "Value source",
@ -1427,8 +1430,11 @@
"ignore-case": "Ignore case",
"condition": "Condition",
"script": "Script",
"add-filter": "Add filter",
"edit-filter": "Edit filter",
"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.",
"conditions": {
"simple": "Simple",
"duration": "Duration",
@ -1476,17 +1482,18 @@
"edit-alarm-rule-additional-info": "Edit additional info",
"alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info",
"alarm-rule-additional-info-hint": "Hint: use <code><span style=\"color: #000;\">$&#123;</span>Argument name<span style=\"color: #000;\">&#125;</span></code> to substitute values of the arguments that are used in alarm rule condition.",
"alarm-rule-additional-info-icon-hint": "Use Argument name to substitute values of the arguments that are used in alarm rule condition.",
"alarm-rule-mobile-dashboard": "Mobile dashboard",
"alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard",
"alarm-rule-no-mobile-dashboard": "No dashboard selected",
"alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Add alarm rule creating condition",
"enter-alarm-rule-clear-condition-prompt": "Add alarm rule clearing condition",
"edit-alarm-rule-condition": "Edit alarm rule condition",
"enter-alarm-rule-condition-prompt": "Add condition",
"enter-alarm-rule-clear-condition-prompt": "Add clearing condition",
"edit-alarm-rule-condition": "Alarm condition",
"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 specified",
"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",
"condition-duration": "Condition duration",
@ -1497,6 +1504,7 @@
"condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.",
"condition-repeating-value": "Count of events",
"condition-repeating-value-hint": "Update of any alarm rule argument will be counted as event",
"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.",
@ -1512,17 +1520,22 @@
"alarm-rule-filter-title": "Filter",
"debugging": "Alarm rule debugging",
"any-type": "Any type",
"enter-alarm-rule-type": "Enter alarm rule type",
"no-alarm-rule-types-matching": "No alarm rule types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm rule types selected.",
"alarm-rule-type-list": "Alarm rule type list",
"enter-alarm-rule-type": "Enter alarm type",
"no-alarm-rule-types-matching": "No alarm types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm types selected.",
"alarm-rule-type-list": "Alarm type list",
"alarm-rule-entity-list": "Entity list",
"missing-for": "missing for",
"time-unit": "Unit",
"value-required": "Value is required.",
"min-value": "Value must be 0 or greater.",
"argument-in-use": "Argument is used as general argument.",
"import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule type."
"import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule structure.",
"no-filter-preview": "No filter specified",
"filter-operation": {
"and": "And",
"or": "Or"
}
},
"ai-models": {
"ai-models": "AI models",
@ -7174,7 +7187,8 @@
"export-relations": "Export relations",
"export-attributes": "Export attributes",
"export-credentials": "Export credentials",
"export-calculated-fields": "Export calculated fields",
"export-calculated-fields": "Export calculated fields \nand alarm rules",
"export-alarm-rules": "Export alarm rules",
"entity-versions": "Entity versions",
"versions": "Versions",
"created-time": "Created time",

Loading…
Cancel
Save