Browse Source

UI: Refactoring and fixed bugs

pull/14276/head
ArtemDzhereleiko 10 months ago
parent
commit
06f5f005a4
  1. 7
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  2. 43
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  3. 42
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts
  4. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  5. 36
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss
  6. 21
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  7. 14
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  8. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  9. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
  10. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
  11. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  12. 208
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  13. 180
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  14. 22
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  15. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
  16. 22
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  17. 36
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
  18. 31
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html
  19. 46
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts
  20. 117
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
  21. 103
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
  22. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  23. 67
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  24. 34
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  25. 46
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  26. 13
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  27. 57
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  28. 114
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  29. 91
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  30. 47
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  31. 84
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
  32. 15
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
  33. 54
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  34. 40
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  35. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  36. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  37. 14
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  38. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  39. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.html
  40. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts
  41. 7
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts
  42. 1
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts
  43. 32
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  44. 16
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  45. 4
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  46. 17
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  47. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

7
ui-ngx/src/app/core/http/calculated-fields.service.ts

@ -15,7 +15,7 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
@ -52,10 +52,7 @@ export class CalculatedFieldsService {
public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable<PageData<CalculatedField>> { public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable<PageData<CalculatedField>> {
let url = `/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`; let url = `/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`;
if (type) { return this.http.get<PageData<CalculatedField>>(url, createDefaultHttpOptions(type ? {type} : null, config));
url += `&type=${type}`;
}
return this.http.get<PageData<CalculatedField>>(url, defaultHttpOptionsFromConfig(config));
} }
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> { public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {

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

@ -15,40 +15,35 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px; max-width: 100%; display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content;"> <form style="width: 800px">
<mat-toolbar color="primary"> <mat-toolbar class="justify-between" color="primary">
<h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2> <h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2>
<span class="flex-1"></span> <button mat-icon-button (click)="cancel()" type="button">
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <div class="flex flex-1 flex-col">
<div class="flex flex-1 flex-col"> <mat-form-field class="mat-block" appearance="outline">
<mat-form-field class="mat-block" appearance="outline"> <textarea matInput [formControl]="alarmDetailsControl" rows="5"
<textarea matInput formControlName="alarmDetails" rows="5" placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
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-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint> </mat-form-field>
</mat-form-field> </div>
</div>
</fieldset>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button *ngIf="!data.readonly" mat-raised-button color="primary" @if (!data.readonly) {
type="submit" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty"> type="button"
{{ 'action.save' | translate }} (click)="save()"
</button> [disabled]="alarmDetailsControl.invalid || !alarmDetailsControl.dirty">
{{ 'action.save' | translate }}
</button>
}
</div> </div>
</form> </form>

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

@ -14,15 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/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';
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { FormBuilder, FormControl } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
export interface AlarmRuleDetailsDialogData { export interface AlarmRuleDetailsDialogData {
alarmDetails: string; alarmDetails: string;
@ -32,51 +30,31 @@ export interface AlarmRuleDetailsDialogData {
@Component({ @Component({
selector: 'tb-edit-alarm-details-dialog', selector: 'tb-edit-alarm-details-dialog',
templateUrl: './alarm-rule-details-dialog.component.html', templateUrl: './alarm-rule-details-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleDetailsDialogComponent}], providers: [],
styleUrls: [] styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
}) })
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string> export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string> {
implements OnInit, ErrorStateMatcher {
alarmDetails = this.data.alarmDetails; alarmDetailsControl: FormControl<string>;
editDetailsFormGroup: UntypedFormGroup;
submitted = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleDetailsDialogComponent, string>, public dialogRef: MatDialogRef<AlarmRuleDetailsDialogComponent, string>,
private fb: UntypedFormBuilder, private fb: FormBuilder) {
public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.editDetailsFormGroup = this.fb.group({ this.alarmDetailsControl = this.fb.control(this.data.alarmDetails);
alarmDetails: [this.alarmDetails]
});
if (this.data.readonly) { if (this.data.readonly) {
this.editDetailsFormGroup.disable(); this.alarmDetailsControl.disable();
} }
} }
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
save(): void { save(): void {
this.submitted = true; this.dialogRef.close(this.alarmDetailsControl.value);
this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value;
this.dialogRef.close(this.alarmDetails);
} }
} }

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

@ -54,7 +54,7 @@
/> />
</div> </div>
</div> </div>
<ng-container [formGroup]="configFormGroup"> <ng-container formGroupName="configuration">
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div> <div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments" <tb-calculated-field-arguments-table formControlName="arguments"

36
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss

@ -32,39 +32,3 @@
color: rgba(0, 0, 0, 0.38); color: rgba(0, 0, 0, 0.38);
min-width: 40px; min-width: 40px;
} }
.tbel-script-lang-chip {
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: white;
border-radius: 100px;
width: 70px;
min-width: 70px;
display: flex;
justify-content: center;
margin-top: 2px;
margin-right: 4px;
}
.tb-js-func {
.ace_tb {
&.ace_calculated-field {
&-ctx {
color: #C52F00;
}
&-args {
color: #185F2A;
}
&-key {
color: #c24c1a;
}
&-time-window, &-values, &-func, &-value, &-ts, &-latestTs {
color: #7214D0;
}
&-start-ts, &-end-ts {
color: #2CAA00;
}
}
}
}

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

@ -55,7 +55,6 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
fieldFormGroup = this.fb.group({ fieldFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
type: [CalculatedFieldType.ALARM],
debugSettings: [], debugSettings: [],
configuration: this.fb.group({ configuration: this.fb.group({
arguments: this.fb.control({}), arguments: this.fb.control({}),
@ -121,22 +120,18 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
const index = keys.indexOf(key); const index = keys.indexOf(key);
if (index >= 0) { if (index >= 0) {
keys.splice(index, 1); keys.splice(index, 1);
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); this.configFormGroup.get('propagateRelationTypes').setValue(keys);
} }
} }
addRelationType(event: MatChipInputEvent): void { addRelationType(event: MatChipInputEvent): void {
const input = event.chipInput.inputElement; const input = event.chipInput.inputElement;
let value = event.value; let value = (event.value ?? '').trim();
if ((value || '').trim()) { if (value) {
value = value.trim(); let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? [];
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value; if (keys.indexOf(value) === -1) {
if (!keys || keys.indexOf(value) === -1) {
if (!keys) {
keys = [];
}
keys.push(value); keys.push(value);
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true}); this.configFormGroup.get('propagateRelationTypes').setValue(keys);
} }
} }
if (input) { if (input) {
@ -164,8 +159,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
} }
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; const { configuration = {}, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); this.fieldFormGroup.patchValue({ configuration, debugSettings, ...value }, {emitEvent: false});
} }
private observeIsLoading(): void { private observeIsLoading(): void {

14
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts

@ -75,20 +75,6 @@ import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/al
], ],
exports: [ exports: [
AlarmRuleDialogComponent, AlarmRuleDialogComponent,
CreateCfAlarmRulesComponent,
CfAlarmRuleComponent,
CfAlarmRuleConditionComponent,
CfAlarmRuleConditionDialogComponent,
CfAlarmScheduleComponent,
CfAlarmScheduleDialogComponent,
AlarmRuleFilterTextComponent,
AlarmRuleFilterListComponent,
AlarmRuleFilterDialogComponent,
AlarmRuleFilterPredicateListComponent,
AlarmRuleFilterPredicateComponent,
AlarmRuleFilterPredicateValueComponent,
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
] ]
}) })
export class AlarmRuleModule { } export class AlarmRuleModule { }

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

@ -42,7 +42,6 @@ import {
CalculatedFieldAlarmRule, CalculatedFieldAlarmRule,
CalculatedFieldType, CalculatedFieldType,
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.service'; import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -128,7 +127,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
name: this.translate.instant('action.export'), name: this.translate.instant('action.export'),
icon: 'file_download', icon: 'file_download',
isEnabled: () => true, isEnabled: () => true,
onAction: (event$, entity) => this.exportCalculatedField(event$, entity), onAction: (event$, entity) => this.exportAlarmRule(event$, entity),
}, },
{ {
name: this.translate.instant('entity-view.events'), name: this.translate.instant('entity-view.events'),
@ -177,6 +176,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
debugSettings, debugSettings,
debugConfig: { debugConfig: {
entityType: EntityType.CALCULATED_FIELD, entityType: EntityType.CALCULATED_FIELD,
isAlarmRule: true,
additionalActionConfig, additionalActionConfig,
}, },
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings) onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
@ -226,7 +226,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
.subscribe(); .subscribe();
} }
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void { private exportAlarmRule($event: Event, calculatedField: CalculatedField): void {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html

@ -15,6 +15,6 @@
limitations under the License. limitations under the License.
--> -->
@if (calculatedFieldsTableConfig) { @if (alarmRulesTableConfig) {
<tb-entities-table [entitiesTableConfig]="calculatedFieldsTableConfig"></tb-entities-table> <tb-entities-table [entitiesTableConfig]="alarmRulesTableConfig"></tb-entities-table>
} }

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss

@ -15,8 +15,6 @@
*/ */
:host ::ng-deep { :host ::ng-deep {
tb-entities-table { tb-entities-table {
.mat-drawer-container { --mat-sidenav-content-background-color: white;
background-color: white;
}
} }
} }

4
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts

@ -52,7 +52,7 @@ export class AlarmRulesTableComponent {
entityName = input<string>(); entityName = input<string>();
ownerId = input<EntityId>(); ownerId = input<EntityId>();
calculatedFieldsTableConfig: AlarmRulesTableConfig; alarmRulesTableConfig: AlarmRulesTableConfig;
constructor(private calculatedFieldsService: CalculatedFieldsService, constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService, private translate: TranslateService,
@ -67,7 +67,7 @@ export class AlarmRulesTableComponent {
effect(() => { effect(() => {
if (this.active()) { if (this.active()) {
this.calculatedFieldsTableConfig = new AlarmRulesTableConfig( this.alarmRulesTableConfig = new AlarmRulesTableConfig(
this.calculatedFieldsService, this.calculatedFieldsService,
this.translate, this.translate,
this.dialog, this.dialog,

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

@ -31,131 +31,123 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <div class="tb-form-panel no-padding no-border">
<div class="tb-form-panel no-padding no-border"> <div class="flex flex-1 flex-col" formGroupName="expression">
<div class="flex flex-1 flex-col" formGroupName="expression"> @if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) { <div class="tb-form-panel">
<div class="tb-form-panel"> <div class="flex flex-row items-center justify-between">
<div class="flex flex-row items-center justify-between"> <div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div>
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div> <tb-toggle-select formControlName="operation"
<tb-toggle-select formControlName="operation" selectMediaBreakpoint="xs">
selectMediaBreakpoint="xs"> <tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option> <tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option>
<tb-toggle-option [value]="ComplexOperation.OR">{{ complexOperationTranslationMap.get(ComplexOperation.OR) | translate }}</tb-toggle-option> </tb-toggle-select>
</tb-toggle-select> </div>
</div> <tb-alarm-rule-filter-list
<tb-alarm-rule-filter-list [operation]="conditionFormGroup.get('expression.operation').value"
[operation]="conditionFormGroup.get('expression.operation').value" [arguments]="arguments"
[arguments]="arguments" formControlName="filters">
formControlName="filters"> </tb-alarm-rule-filter-list>
</tb-alarm-rule-filter-list> </div>
} @else {
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">
{{ 'alarm-rule.script' | translate }}
</div> </div>
} @else { <tb-js-func formControlName="expression"
<div class="tb-form-panel no-gap"> functionName="expression"
<div class="tb-form-panel-title tb-required"> [disableUndefinedCheck]="true"
{{ 'alarm-rule.script' | translate }} [scriptLanguage]="scriptLanguage.TBEL"
[functionArgs]="functionArgs"
[highlightRules]="argumentsHighlightRules"
[editorCompleter]="argumentsEditorCompleter"
noValidate="true">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.tbel' | translate }}
</div> </div>
<tb-js-func formControlName="expression" </tb-js-func>
functionName="expression" </div>
[disableUndefinedCheck]="true" }
[scriptLanguage]="scriptLanguage.TBEL" </div>
[functionArgs]="functionArgs" <section class="tb-form-panel">
[highlightRules]="argumentsHighlightRules" <div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
[editorCompleter]="argumentsEditorCompleter" <mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
noValidate="true"> <mat-label translate>alarm-rule.condition-type</mat-label>
<div toolbarPrefixButton <mat-select formControlName="type" required>
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.tbel' | translate }} <mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
</div> {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</tb-js-func> </mat-option>
</mat-select>
</mat-form-field>
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
<tb-toggle-select [formControl]="durationDynamicModeControl"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div> </div>
} <div class="flex flex-row gap-2 xs:flex-col">
</div> <div class="flex-1" [class.!hidden]="durationDynamicModeControl.value">
<section class="tb-form-panel"> <ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container>
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div> </div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic"> <div class="flex-1" [class.!hidden]="!durationDynamicModeControl.value">
<mat-label translate>alarm-rule.condition-type</mat-label> <ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'value'}"></ng-container>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@if (conditionFormGroup.get('type').value == AlarmConditionType.DURATION) {
<div class="tb-form-panel stroked no-padding-bottom">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
<tb-toggle-select [ngModel]="durationDynamicMode"
[ngModelOptions]="{standalone: true}"
(ngModelChange)="toggleDynamicMode(AlarmConditionType.DURATION)"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div> </div>
<div class="flex flex-row gap-2 xs:flex-col"> <div class="max-w-23% flex-full xs:max-w-full">
<div class="flex-1" [class.!hidden]="durationDynamicMode"> <mat-form-field class="mat-block" appearance="outline">
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container> <mat-select formControlName="unit" required
</div> placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}">
<div class="flex-1" [class.!hidden]="!durationDynamicMode"> <mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'value'}"></ng-container> {{ timeUnitTranslations.get(timeUnit) | translate }}
</div> </mat-option>
<div class="max-w-23% flex-full xs:max-w-full"> </mat-select>
<mat-form-field class="mat-block" appearance="outline"> <mat-error *ngIf="conditionFormGroup.get('unit').hasError('required')">
<mat-select formControlName="unit" required {{ 'alarm-rule.condition-duration-time-unit-required' | translate }}
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}"> </mat-error>
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> </mat-form-field>
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('unit').hasError('required')">
{{ 'alarm-rule.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div> </div>
</div> </div>
} @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) { </div>
<div class="tb-form-panel stroked"> } @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) {
<div class="flex flex-row items-center justify-between"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div> <div class="flex flex-row items-center justify-between">
<tb-toggle-select [ngModel]="repeatingDynamicMode" <div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
[ngModelOptions]="{standalone: true}" <tb-toggle-select [formControl]="repeatingDynamicModeControl" selectMediaBreakpoint="xs">
(ngModelChange)="toggleDynamicMode(AlarmConditionType.REPEATING)" <tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option>
selectMediaBreakpoint="xs"> <tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="false">{{ 'alarm-rule.static' | translate }}</tb-toggle-option> </tb-toggle-select>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic' | translate }}</tb-toggle-option> </div>
</tb-toggle-select> <div class="flex flex-1 flex-row gap-2">
<div class="flex-1" [class.!hidden]="repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div> </div>
<div class="flex flex-1 flex-row gap-2"> <div class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value">
<div class="flex-1" [class.!hidden]="repeatingDynamicMode"> <ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container>
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.REPEATING, groupName: 'count'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!repeatingDynamicMode">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container>
</div>
</div> </div>
</div> </div>
} </div>
</section> }
</div> </section>
</fieldset> </div>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} {{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
*ngIf="!readonly" <button mat-raised-button color="primary"
type="submit" *ngIf="!readonly"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty"> type="submit"
{{ 'action.save' | translate }} [disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty">
</button> {{ 'action.save' | translate }}
</button>
}
</div> </div>
<ng-template #staticValueTemplate let-type="type" let-groupName="groupName"> <ng-template #staticValueTemplate let-type="type" let-groupName="groupName">

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

@ -14,22 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/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';
import { import { FormBuilder, Validators } from '@angular/forms';
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
Validators
} from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models'; import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models"; import { ScriptLanguage } from "@shared/models/rule-node.models";
@ -47,7 +38,6 @@ import {
import { TbEditorCompleter } from "@shared/models/ace/completion.models"; import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models"; import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models"; import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
export interface CfAlarmRuleConditionDialogData { export interface CfAlarmRuleConditionDialogData {
readonly: boolean; readonly: boolean;
@ -58,11 +48,10 @@ export interface CfAlarmRuleConditionDialogData {
@Component({ @Component({
selector: 'tb-cf-alarm-rule-condition-dialog', selector: 'tb-cf-alarm-rule-condition-dialog',
templateUrl: './cf-alarm-rule-condition-dialog.component.html', templateUrl: './cf-alarm-rule-condition-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmRuleConditionDialogComponent}], providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'], styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
}) })
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition> export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition> {
implements OnInit, ErrorStateMatcher {
AlarmRuleExpressionType = AlarmRuleExpressionType; AlarmRuleExpressionType = AlarmRuleExpressionType;
@ -74,9 +63,24 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
readonly = this.data.readonly; readonly = this.data.readonly;
condition = this.data.condition; condition = this.data.condition;
conditionFormGroup: FormGroup<FormControlsFrom<AlarmRuleCondition>>; conditionFormGroup = this.fb.group({
expression: this.fb.group({
submitted = false; type: [AlarmRuleExpressionType.SIMPLE],
expression: ['', [Validators.required]],
operation: [ComplexOperation.AND],
filters: [null],
}),
type: this.fb.control(AlarmRuleConditionType.SIMPLE, Validators.required),
unit: this.fb.control(TimeUnit.SECONDS, Validators.required),
value: this.fb.group({
staticValue: this.fb.control<number | null>(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]),
dynamicValueArgument: this.fb.control<string | null>(null, Validators.required),
}),
count: this.fb.group({
staticValue: this.fb.control<number | null>(null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]),
dynamicValueArgument: this.fb.control<string | null>(null, Validators.required),
}),
});
readonly scriptLanguage = ScriptLanguage; readonly scriptLanguage = ScriptLanguage;
@ -85,8 +89,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
defaultValueRangeError = ''; defaultValueRangeError = '';
defaultValuePatternError = ''; defaultValuePatternError = '';
durationDynamicMode = !!this.condition?.value?.dynamicValueArgument; durationDynamicModeControl = this.fb.control<boolean>(false);
repeatingDynamicMode = !!this.condition?.count?.dynamicValueArgument; repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation; ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = complexOperationTranslationMap;
@ -96,38 +100,41 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
argumentsHighlightRules: AceHighlightRules; argumentsHighlightRules: AceHighlightRules;
arguments = this.data.arguments; arguments = this.data.arguments;
argumentsList: Array<string>;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData, @Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>, public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: UntypedFormBuilder, private fb: FormBuilder) {
public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)]; this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments); this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments);
this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments); this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments);
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.conditionFormGroup = this.fb.group({
expression: this.fb.group({ this.conditionFormGroup.patchValue({
type: [this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE], expression: {
expression: [this.condition?.expression?.expression ?? null, [Validators.required]], type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
operation: [this.condition?.expression?.operation ?? ComplexOperation.AND], expression: this.condition?.expression?.expression ?? null,
filters: [this.condition?.expression?.filters], filters: this.condition?.expression?.filters ?? [],
}), operation: this.condition?.expression?.operation ?? ComplexOperation.AND
type: [this.condition?.type ?? AlarmRuleConditionType.SIMPLE, Validators.required], },
unit: [this.condition?.unit ?? TimeUnit.SECONDS, Validators.required], type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
value: this.fb.group({ unit: this.condition?.unit ?? TimeUnit.SECONDS,
staticValue: [this.condition?.value?.staticValue ?? null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], value: {
dynamicValueArgument: [this.condition?.value?.dynamicValueArgument ?? null, Validators.required], staticValue: this.condition?.value?.staticValue,
}), dynamicValueArgument: this.condition?.value?.dynamicValueArgument,
count: this.fb.group({ },
staticValue: [this.condition?.count?.staticValue ?? null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]], count: {
dynamicValueArgument: [this.condition?.count?.dynamicValueArgument ?? null, Validators.required], staticValue: this.condition?.count?.staticValue ?? null,
}), dynamicValueArgument: this.condition?.count?.dynamicValueArgument
}); }
}, {emitEvent: false});
this.durationDynamicModeControl.patchValue(!!this.condition?.value?.dynamicValueArgument, {emitEvent: false});
this.repeatingDynamicModeControl.patchValue(!!this.condition?.count?.dynamicValueArgument, {emitEvent: false});
this.conditionFormGroup.get('type').valueChanges.pipe( this.conditionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
@ -142,93 +149,70 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
}); });
this.durationDynamicModeControl.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((mode) => {
this.updateStaticValueValidator(AlarmRuleConditionType.DURATION, mode);
});
this.repeatingDynamicModeControl.valueChanges.pipe(
takeUntilDestroyed()
).subscribe((mode) => {
this.updateStaticValueValidator(AlarmRuleConditionType.REPEATING, mode);
});
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE); this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE'); this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
} }
ngOnInit(): void {
}
toggleDynamicMode(type: AlarmRuleConditionType): void {
if (type === AlarmRuleConditionType.DURATION) {
this.durationDynamicMode = !this.durationDynamicMode;
this.updateStaticValueValidator(type, this.durationDynamicMode);
} else {
this.repeatingDynamicMode = !this.repeatingDynamicMode;
this.updateStaticValueValidator(type, this.repeatingDynamicMode);
}
}
updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) { updateStaticValueValidator(type: AlarmRuleConditionType, dynamicValue: boolean) {
const control = type === AlarmRuleConditionType.DURATION ? 'value' : 'count'; const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count');
if (dynamicValue) { if (dynamicValue) {
this.conditionFormGroup.get(`${control}.staticValue`).disable({emitEvent: false}); control.get('staticValue').disable({emitEvent: false});
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).enable({emitEvent: false}); control.get('dynamicValueArgument').enable({emitEvent: false});
} else { } else {
this.conditionFormGroup.get(`${control}.staticValue`).enable({emitEvent: false}); control.get('staticValue').enable({emitEvent: false});
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).disable({emitEvent: false}); control.get('dynamicValueArgument').disable({emitEvent: false});
} }
this.conditionFormGroup.get(`${control}.staticValue`).updateValueAndValidity({emitEvent: false})
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).updateValueAndValidity({emitEvent: false})
} }
updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') { updateExpressionTypeValidator(type: 'SIMPLE' | 'TBEL') {
if (type === 'SIMPLE') { if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable(); this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
this.conditionFormGroup.get(`expression.filters`).enable(); this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false});
} else { } else {
this.conditionFormGroup.get(`expression.expression`).enable(); this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false});
this.conditionFormGroup.get(`expression.filters`).disable(); this.conditionFormGroup.get(`expression.filters`).disable({emitEvent: false});
} }
this.conditionFormGroup.get(`expression.expression`).updateValueAndValidity({emitEvent: false});
this.conditionFormGroup.get(`expression.filters`).updateValueAndValidity({emitEvent: false});
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
} }
private updateValidators(type: AlarmRuleConditionType, emitEvent = false) { private updateValidators(type: AlarmRuleConditionType, emitEvent = false) {
switch (type) { switch (type) {
case AlarmRuleConditionType.DURATION: case AlarmRuleConditionType.DURATION:
this.conditionFormGroup.get('unit').enable(); this.conditionFormGroup.get('unit').enable({emitEvent: false});
this.conditionFormGroup.get('value').enable(); this.conditionFormGroup.get('value').enable({emitEvent: false});
this.conditionFormGroup.get('count').disable(); this.conditionFormGroup.get('count').disable({emitEvent: false});
this.updateStaticValueValidator(type, this.durationDynamicModeControl.value);
this.updateStaticValueValidator(type, this.durationDynamicMode);
this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value'; this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value';
this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required'; this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required';
this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range'; this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range';
this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern'; this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern';
break; break;
case AlarmRuleConditionType.REPEATING: case AlarmRuleConditionType.REPEATING:
this.conditionFormGroup.get('count').enable(); this.conditionFormGroup.get('count').enable({emitEvent: false});
this.conditionFormGroup.get('value').disable(); this.conditionFormGroup.get('value').disable({emitEvent: false});
this.conditionFormGroup.get('unit').disable(); this.conditionFormGroup.get('unit').disable({emitEvent: false});
this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value);
this.updateStaticValueValidator(type, this.repeatingDynamicMode);
this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value'; this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value';
this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required'; this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required';
this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range'; this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range';
this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern'; this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern';
break; break;
case AlarmRuleConditionType.SIMPLE: case AlarmRuleConditionType.SIMPLE:
this.conditionFormGroup.get('value').disable(); this.conditionFormGroup.get('value').disable({emitEvent: false});
this.conditionFormGroup.get('count').disable(); this.conditionFormGroup.get('count').disable({emitEvent: false});
this.conditionFormGroup.get('unit').disable(); this.conditionFormGroup.get('unit').disable({emitEvent: false});
break; break;
} }
this.conditionFormGroup.get('value').updateValueAndValidity({emitEvent});
this.conditionFormGroup.get('count').updateValueAndValidity({emitEvent});
this.conditionFormGroup.get('unit').updateValueAndValidity({emitEvent});
}
get argumentsList(): Array<string> {
return this.arguments ? Object.keys(this.arguments): [];
} }
cancel(): void { cancel(): void {
@ -236,9 +220,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
} }
save(): void { save(): void {
this.submitted = true; this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
this.condition = this.conditionFormGroup.value as AlarmRuleCondition;
this.dialogRef.close(this.condition);
} }
} }

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

@ -14,12 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
Validator, Validator,
Validators Validators
@ -40,6 +40,7 @@ import {
import { import {
AlarmRuleCondition, AlarmRuleCondition,
AlarmRuleConditionType, AlarmRuleConditionType,
AlarmRuleExpressionType,
AlarmRuleSchedule, AlarmRuleSchedule,
AlarmRuleScheduleType AlarmRuleScheduleType
} from "@shared/models/alarm-rule.models"; } from "@shared/models/alarm-rule.models";
@ -48,6 +49,7 @@ import {
AlarmRuleScheduleDialogData, AlarmRuleScheduleDialogData,
CfAlarmScheduleDialogComponent CfAlarmScheduleDialogComponent
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component"; } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({ @Component({
selector: 'tb-cf-alarm-rule-condition', selector: 'tb-cf-alarm-rule-condition',
@ -66,9 +68,10 @@ import {
} }
] ]
}) })
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator { export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Validator {
@Input() @Input()
@coerceBoolean()
disabled: boolean; disabled: boolean;
@Input() @Input()
@ -76,7 +79,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
alarmRuleConditionFormGroup = this.fb.group({ alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'], type: ['SIMPLE'],
expression: [null, Validators.required], expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required],
schedule: [null], schedule: [null],
}); });
@ -89,7 +92,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog, constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder, private fb: FormBuilder,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private translate: TranslateService) { private translate: TranslateService) {
} }
@ -101,9 +104,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
ngOnInit() {
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
@ -119,7 +119,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
} }
public conditionSet() { public conditionSet() {
return this.modelValue && (this.modelValue.expression.expression || this.modelValue.expression.filters); return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
} }
public validate(c: UntypedFormControl) { public validate(c: UntypedFormControl) {
@ -147,7 +147,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
if (result) { if (result) {
this.modelValue = {...this.modelValue, ...result}; this.modelValue = {...this.modelValue, ...result};
this.updateModel(); this.updateModel();
this.updateSpecText();
this.cd.detectChanges(); this.cd.detectChanges();
} }
}); });
@ -233,8 +232,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
}).afterClosed().subscribe((result) => { }).afterClosed().subscribe((result) => {
if (result) { if (result) {
this.modelValue.schedule = result; this.modelValue.schedule = result;
this.propagateChange(this.modelValue); this.updateModel();
this.updateScheduleText();
this.cd.detectChanges(); this.cd.detectChanges();
} }
}); });

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

@ -14,10 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
min-width: 0;
.row {
margin-top: 1em;
}
.tb-alarm-rule-details, .tb-alarm-rule-dashboard { .tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px; padding: 4px;
&.title { &.title {

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

@ -17,15 +17,14 @@
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { DashboardId } from '@shared/models/id/dashboard-id'; import { DashboardId } from '@shared/models/id/dashboard-id';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -35,6 +34,7 @@ import {
AlarmRuleDetailsDialogComponent, AlarmRuleDetailsDialogComponent,
AlarmRuleDetailsDialogData AlarmRuleDetailsDialogData
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component"; } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({ @Component({
selector: 'tb-cf-alarm-rule', selector: 'tb-cf-alarm-rule',
@ -56,24 +56,20 @@ import {
export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator { export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
@Input() @Input()
@coerceBoolean()
disabled: boolean; disabled: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input() @Input()
set required(value: boolean) { @coerceBoolean()
this.requiredValue = coerceBooleanProperty(value); required: boolean;
}
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
private modelValue: AlarmRule; private modelValue: AlarmRule;
alarmRuleFormGroup= this.fb.group({ alarmRuleFormGroup = this.fb.group({
condition: [null, [Validators.required]], condition: this.fb.control({}, Validators.required),
alarmDetails: [null], alarmDetails: [null],
dashboardId: [null] dashboardId: [null]
}); });
@ -81,7 +77,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog, constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder, private fb: FormBuilder,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
} }
@ -147,7 +143,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
private updateModel() { private updateModel() {
const value = this.alarmRuleFormGroup.value; const value = this.alarmRuleFormGroup.value;
this.modelValue = {...this.modelValue, ...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null}; this.modelValue = {...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null} as AlarmRule;
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
} }
} }

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

@ -20,5 +20,41 @@
display: grid; display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content; grid-template-rows: min-content minmax(auto, 1fr) min-content;
} }
.tbel-script-lang-chip {
line-height: 20px;
font-size: 14px;
font-weight: 500;
color: white;
border-radius: 100px;
width: 70px;
min-width: 70px;
display: flex;
justify-content: center;
margin-top: 2px;
margin-right: 4px;
}
.tb-js-func {
.ace_tb {
&.ace_calculated-field {
&-ctx {
color: #C52F00;
}
&-args {
color: #185F2A;
}
&-key {
color: #c24c1a;
}
&-time-window, &-values, &-func, &-value, &-ts, &-latestTs {
color: #7214D0;
}
&-start-ts, &-end-ts {
color: #2CAA00;
}
}
}
}
} }

31
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html

@ -15,13 +15,13 @@
limitations under the License. limitations under the License.
--> -->
<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()"> <form>
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}</h2> <h2>{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
<tb-toggle-select [(ngModel)]="settingsMode" [ngModelOptions]="{standalone: true}" appearance="fill-invert" selectMediaBreakpoint="xs"> <tb-toggle-select [formControl]="dynamicModeControl" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option value="static">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option> <tb-toggle-option [value]="false">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option>
<tb-toggle-option value="dynamic">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option> <tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
@ -29,26 +29,25 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<tb-cf-alarm-schedule [arguments]="arguments" <tb-cf-alarm-schedule [arguments]="arguments"
[settingsMode]="settingsMode" [dynamicMode]="dynamicModeControl.value"
formControlName="alarmSchedule"> [formControl]="alarmScheduleControl">
</tb-cf-alarm-schedule> </tb-cf-alarm-schedule>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} {{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
*ngIf="!readonly" <button mat-raised-button color="primary"
type="submit" type="button"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty"> (click)="save()"
{{ 'action.save' | translate }} [disabled]="alarmScheduleControl.invalid || !alarmScheduleControl.dirty">
</button> {{ 'action.save' | translate }}
</button>
}
</div> </div>
</form> </form>

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

@ -14,16 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/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';
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models"; import { AlarmRuleSchedule } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@ -36,57 +33,38 @@ export interface AlarmRuleScheduleDialogData {
@Component({ @Component({
selector: 'tb-cf-alarm-schedule-dialog', selector: 'tb-cf-alarm-schedule-dialog',
templateUrl: './cf-alarm-schedule-dialog.component.html', templateUrl: './cf-alarm-schedule-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmScheduleDialogComponent}], providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'], styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
}) })
export class CfAlarmScheduleDialogComponent extends DialogComponent<CfAlarmScheduleDialogComponent, AlarmRuleSchedule> export class CfAlarmScheduleDialogComponent extends DialogComponent<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>{
implements OnInit, ErrorStateMatcher {
readonly = this.data.readonly; readonly = this.data.readonly;
alarmSchedule = this.data.alarmSchedule; alarmSchedule = this.data.alarmSchedule;
arguments = this.data.arguments; arguments = this.data.arguments;
alarmScheduleFormGroup: UntypedFormGroup; alarmScheduleControl = this.fb.control<AlarmRuleSchedule>(null);
submitted = false; dynamicModeControl = this.fb.control(false);
settingsMode: 'static' | 'dynamic' = 'static';
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>, public dialogRef: MatDialogRef<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>,
private fb: UntypedFormBuilder, private fb: FormBuilder) {
private utils: UtilsService,
public translate: TranslateService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.alarmScheduleControl.patchValue(this.alarmSchedule, {emitEvent: false});
this.alarmScheduleFormGroup = this.fb.group({ this.dynamicModeControl.patchValue(!!this.alarmSchedule?.dynamicValueArgument, {emitEvent: false});
alarmSchedule: [this.alarmSchedule]
});
this.settingsMode = this.alarmSchedule?.dynamicValueArgument ? 'dynamic' : 'static';
if (this.readonly) { if (this.readonly) {
this.alarmScheduleFormGroup.disable({emitEvent: false}); this.alarmScheduleControl.disable({emitEvent: false});
this.dynamicModeControl.disable({emitEvent: false});
} }
} }
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
save(): void { save(): void {
this.submitted = true; this.dialogRef.close(this.alarmScheduleControl.value);
this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value;
this.dialogRef.close(this.alarmSchedule);
} }
} }

117
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html

@ -16,17 +16,17 @@
--> -->
<section [formGroup]="alarmScheduleForm" class="flex flex-col"> <section [formGroup]="alarmScheduleForm" class="flex flex-col">
@if (settingsMode === 'static') { @if (!dynamicMode) {
<ng-container formGroupName="staticValue"> <ng-container formGroupName="staticValue">
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="type" required placeholder="{{ 'alarm-rule.schedule-type' | translate }}"> <mat-select formControlName="type" required placeholder="{{ 'alarm-rule.schedule-type' | translate }}">
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType"> @for (scheduleType of alarmScheduleTypes; track scheduleType) {
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} <mat-option [value]="scheduleType">{{ alarmScheduleTypeTranslate.get(scheduleType) | translate }}</mat-option>
</mat-option> }
</mat-select> </mat-select>
<mat-error *ngIf="alarmScheduleForm.get('staticValue.type').hasError('required')"> @if (alarmScheduleForm.get('staticValue.type').hasError('required')) {
{{ 'alarm-rule.schedule-type-required' | translate }} <mat-error>{{ 'alarm-rule.schedule-type-required' | translate }}</mat-error>
</mat-error> }
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>
@if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) { @if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) {
@ -38,66 +38,67 @@
required required
formControlName="timezone"> formControlName="timezone">
</tb-timezone-select> </tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME"> @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) {
<mat-chip-listbox multiple formControlName="daysOfWeek"> <section>
<mat-chip-option *ngFor="let day of allDays; let i = index;" <mat-chip-listbox multiple formControlName="daysOfWeek">
[value]="i+1"> <mat-chip-option *ngFor="let day of allDays; let i = index;"
{{ dayOfWeekTranslationsArray[day] | translate }} [value]="i+1">
</mat-chip-option> {{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-listbox> </mat-chip-option>
<tb-error class="block mb-2" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required') </mat-chip-listbox>
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> <tb-error class="block mb-2" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required')
<div class="flex flex-row xs:flex-col gt-xs:gap-2"> ? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
<div class="flex flex-row gap-2 gt-md:flex-1"> <div class="flex flex-row xs:flex-col gt-xs:gap-2">
<mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5" appearance="outline"> <div class="flex flex-row gap-2 gt-md:flex-1">
<mat-label translate>alarm-rule.schedule-time-from</mat-label> <mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5" appearance="outline">
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field>
<mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5" appearance="outline">
<mat-label translate>alarm-rule.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div>
<div class="flex flex-1 items-center justify-center mb-[22px]">
<div style="text-align: center"
[innerHTML]="getSchedulerRangeText(alarmScheduleForm.get('staticValue'))">
</div>
</div>
</div>
</section>
<section *ngIf="alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM" class="flex flex-col gap-4">
<div *ngFor="let day of allDays" class="flex flex-col gap-4" formArrayName="items">
<div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day">
<mat-chip-listbox formControlName="enabled">
<mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option>
</mat-chip-listbox>
<div class="flex flex-1 flex-row gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.schedule-time-from</mat-label> <mat-label translate>alarm-rule.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5" appearance="outline">
<mat-label translate>alarm-rule.schedule-time-to</mat-label> <mat-label translate>alarm-rule.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-1 items-center justify-center sm:max-w-[120px] mb-[22px]" <div class="flex flex-1 items-center justify-center mb-[22px]">
style="text-align: center" <div class="text-center" [innerHTML]="getSchedulerRangeText(alarmScheduleForm.get('staticValue'))"></div>
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div> </div>
</div> </div>
</div> </section>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks') }
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) {
</section> <section class="flex flex-col gap-4">
<div *ngFor="let day of allDays" class="flex flex-col gap-4" formArrayName="items">
<div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day">
<mat-chip-listbox formControlName="enabled">
<mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option>
</mat-chip-listbox>
<div class="flex flex-1 flex-row gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div>
<div class="flex flex-1 items-center justify-center sm:max-w-[120px] mb-[22px] text-center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div>
</div>
<tb-error class="block" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section>
}
</div> </div>
</div> </div>
} }
@ -110,11 +111,11 @@
<mat-option [value]="argument">{{ argument }}</mat-option> <mat-option [value]="argument">{{ argument }}</mat-option>
} }
</mat-select> </mat-select>
<mat-error *ngIf="alarmScheduleForm.get('dynamicValueArgument').hasError('required')"> @if (alarmScheduleForm.get('dynamicValueArgument').hasError('required')) {
{{ 'calculated-fields.hint.argument-name-required' | translate }} <mat-error>{{ 'calculated-fields.hint.argument-name-required' | translate }}</mat-error>
</mat-error> }
</mat-form-field> </mat-form-field>
<div matSuffix style="height: 56px" <div matSuffix class="h-14"
[tb-help-popup]="'alarm-rule/alarm_rule_schedule_format'" [tb-help-popup]="'alarm-rule/alarm_rule_schedule_format'"
tb-help-popup-placement="left" tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}"> [tb-help-popup-style]="{maxWidth: '970px'}">

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

@ -14,20 +14,21 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { import {
CustomTimeSchedulerItem,
dayOfWeekTranslations, dayOfWeekTranslations,
getAlarmScheduleRangeText, getAlarmScheduleRangeText,
timeOfDayToUTCTimestamp, timeOfDayToUTCTimestamp,
@ -43,6 +44,7 @@ import {
} 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 { MatChipSelectionChange } from "@angular/material/chips"; import { MatChipSelectionChange } from "@angular/material/chips";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({ @Component({
selector: 'tb-cf-alarm-schedule', selector: 'tb-cf-alarm-schedule',
@ -58,43 +60,38 @@ import { MatChipSelectionChange } from "@angular/material/chips";
multi: true multi: true
}] }]
}) })
export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit { export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input() @Input()
@coerceBoolean()
disabled: boolean; disabled: boolean;
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
private settingsModeValue: 'static' | 'dynamic';
get settingsMode(): 'static' | 'dynamic' {
return this.settingsModeValue;
}
@Input() @Input()
set settingsMode(value: 'static' | 'dynamic') { @coerceBoolean()
if (value !== this.settingsModeValue && this.alarmScheduleForm) { dynamicMode: boolean;
this.settingsModeValue = value;
this.updateModeValidators(value);
this.updateModel();
}
}
alarmScheduleForm = this.fb.group({ alarmScheduleForm = this.fb.group({
staticValue: this.fb.group({ staticValue: this.fb.group({
type: [AlarmRuleScheduleType.ANY_TIME, Validators.required], type: [AlarmRuleScheduleType.ANY_TIME, Validators.required],
timezone: [null, Validators.required], timezone: ['', Validators.required],
daysOfWeek: [null, Validators.required], daysOfWeek: this.fb.control<number[] | null>(null, Validators.required),
startsOn: [0, Validators.required], startsOn: this.fb.control<Date | number>(0, Validators.required),
endsOn: [0, Validators.required], endsOn: this.fb.control<Date | number>(0, Validators.required),
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems), items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems),
}), }),
dynamicValueArgument: [null, Validators.required] dynamicValueArgument: ['', Validators.required]
}); });
alarmScheduleTypes = Object.keys(AlarmRuleScheduleType); alarmScheduleTypes = Object.keys(AlarmRuleScheduleType) as Array<AlarmRuleScheduleType>;
alarmScheduleType = AlarmRuleScheduleType; alarmScheduleType = AlarmRuleScheduleType;
alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap; alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations; dayOfWeekTranslationsArray = dayOfWeekTranslations;
argumentsList: Array<string>;
allDays = Array(7).fill(0).map((x, i) => i); allDays = Array(7).fill(0).map((x, i) => i);
private modelValue: AlarmRuleSchedule; private modelValue: AlarmRuleSchedule;
@ -102,23 +99,24 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
private defaultItems = Array.from({length: 7}, (value, i) => ({ private defaultItems = Array.from({length: 7}, (value, i) => ({
enabled: true, enabled: true,
dayOfWeek: i + 1 dayOfWeek: i + 1
})); })) as CustomTimeSchedulerItem[];
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder, constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
} }
ngOnInit(): void { ngOnInit(): void {
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe( this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((type) => { ).subscribe((type) => {
const defaultTimezone = getDefaultTimezone(); const defaultTimezone = getDefaultTimezone();
this.alarmScheduleForm.get('staticValue').patchValue({type, items: this.defaultItems, timezone: defaultTimezone}, {emitEvent: false}); const staticValue = {...this.alarmScheduleForm.get('staticValue').value, type, items: this.defaultItems, timezone: defaultTimezone};
this.alarmScheduleForm.get('staticValue').patchValue(staticValue, {emitEvent: false});
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false}); this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false});
this.updateValidators(type); this.updateValidators(type);
this.alarmScheduleForm.updateValueAndValidity();
}); });
this.alarmScheduleForm.valueChanges.pipe( this.alarmScheduleForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
@ -133,6 +131,16 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
}); });
} }
ngOnChanges(changes: SimpleChanges) {
if (changes.dynamicMode) {
const dynamicModeChanges = changes.dynamicMode;
if (!dynamicModeChanges.firstChange && dynamicModeChanges.currentValue !== dynamicModeChanges.previousValue) {
this.updateModeValidators(dynamicModeChanges.currentValue);
this.updateModel();
}
}
}
validateItems(control: AbstractControl): ValidationErrors | null { validateItems(control: AbstractControl): ValidationErrors | null {
const items: any[] = control.value; const items: any[] = control.value;
if (!items || !items.length || !items.find(v => v.enabled === true)) { if (!items || !items.length || !items.find(v => v.enabled === true)) {
@ -155,7 +163,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (this.disabled) { if (this.disabled) {
this.alarmScheduleForm.disable({emitEvent: false}); this.alarmScheduleForm.disable({emitEvent: false});
} else { } else {
this.updateModeValidators(this.settingsMode); this.updateModeValidators(this.dynamicMode);
} }
} }
@ -163,10 +171,8 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (value) { if (value) {
this.modelValue = value; this.modelValue = value;
if (this.modelValue.dynamicValueArgument) { if (this.modelValue.dynamicValueArgument) {
this.settingsModeValue = 'dynamic';
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false}); this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false});
} else { } else {
this.settingsModeValue = 'static';
switch (this.modelValue.staticValue.type) { switch (this.modelValue.staticValue.type) {
case AlarmRuleScheduleType.SPECIFIC_TIME: case AlarmRuleScheduleType.SPECIFIC_TIME:
this.alarmScheduleForm.patchValue({ this.alarmScheduleForm.patchValue({
@ -212,11 +218,11 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
} }
this.updateValidators(this.modelValue.staticValue.type); this.updateValidators(this.modelValue.staticValue.type);
} }
this.updateModeValidators(this.settingsMode); this.updateModeValidators(this.dynamicMode);
} }
} }
validate(control: UntypedFormGroup): ValidationErrors | null { validate(control: FormGroup): ValidationErrors | null {
return this.alarmScheduleForm.valid ? null : { return this.alarmScheduleForm.valid ? null : {
alarmScheduler: { alarmScheduler: {
valid: false valid: false
@ -224,14 +230,14 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
}; };
} }
private updateModeValidators(mode: 'static' | 'dynamic') { private updateModeValidators(mode: boolean) {
if (mode === 'static') { if (mode) {
this.alarmScheduleForm.get('staticValue').disable({emitEvent: false});
this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false});
} else {
this.alarmScheduleForm.get('staticValue').enable({emitEvent: false}); this.alarmScheduleForm.get('staticValue').enable({emitEvent: false});
this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false}); this.alarmScheduleForm.get('dynamicValueArgument').disable({emitEvent: false});
this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value); this.updateValidators(this.alarmScheduleForm.get('staticValue.type').value);
} else {
this.alarmScheduleForm.get('staticValue').disable({emitEvent: false});
this.alarmScheduleForm.get('dynamicValueArgument').enable({emitEvent: false});
} }
} }
@ -256,14 +262,14 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false}); this.alarmScheduleForm.get('staticValue.daysOfWeek').disable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false}); this.alarmScheduleForm.get('staticValue.startsOn').disable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false}); this.alarmScheduleForm.get('staticValue.endsOn').disable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: true}); this.alarmScheduleForm.get('staticValue.items').enable({emitEvent: false});
break; break;
} }
} }
private updateModel() { private updateModel() {
const value = this.alarmScheduleForm.value as AlarmRuleSchedule; const value = this.alarmScheduleForm.value as AlarmRuleSchedule;
if (this.settingsMode === 'static') { if (!this.dynamicMode) {
if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) { if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) {
value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn); value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn);
} }
@ -271,22 +277,18 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn); value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn);
} }
if (isDefined(value.staticValue.items)){ if (isDefined(value.staticValue.items)){
value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items; value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items as CustomTimeSchedulerItem[];
value.staticValue.items = value.staticValue.items.map((item) => { value.staticValue.items = value.staticValue.items.map((item) => {
return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)}; return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)};
}); });
} }
} }
this.modelValue = value; this.modelValue = value;
if (this.alarmScheduleForm.valid) { this.propagateChange(this.modelValue);
this.propagateChange(this.modelValue);
} else {
this.propagateChange(null);
}
} }
private defaultItemsScheduler(index): UntypedFormGroup { private defaultItemsScheduler(index: number): FormGroup {
return this.fb.group({ return this.fb.group({
enabled: [true], enabled: [true],
dayOfWeek: [index + 1], dayOfWeek: [index + 1],
@ -310,16 +312,11 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
} }
} }
getSchedulerRangeText(control: UntypedFormGroup | AbstractControl): string { getSchedulerRangeText(control: FormGroup | AbstractControl): string {
return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value); return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value);
} }
get itemsSchedulerForm(): UntypedFormArray { get itemsSchedulerForm(): FormArray {
return this.alarmScheduleForm.get('staticValue.items') as UntypedFormArray; return this.alarmScheduleForm.get('staticValue.items') as FormArray;
} }
get argumentsList(): Array<string> {
return this.arguments ? Object.keys(this.arguments): [];
}
} }

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

@ -15,27 +15,12 @@
limitations under the License. limitations under the License.
--> -->
<!--
Copyright © 2016-2025 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
@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" [style.border-left-color]="AlarmSeverityNotificationColors.get(createAlarmRuleControl.get('severity').value)"> <div class="create-alarm-rule flex flex-1 flex-col gap-4"
[style.border-left-color]="AlarmSeverityNotificationColors.get(createAlarmRuleControl.get('severity').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" translate>alarm.severity</div> <div class="min-w-40 xs:min-w-fit" translate>alarm.severity</div>
<mat-form-field appearance="outline" class="flex-1 xs:w-full" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="flex-1 xs:w-full" subscriptSizing="dynamic">

67
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts

@ -14,25 +14,25 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Subject } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models'; import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
import { takeUntil } from 'rxjs/operators';
import { AlarmRule } from "@shared/models/alarm-rule.models"; import { AlarmRule } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models"; import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({ @Component({
selector: 'tb-create-cf-alarm-rules', selector: 'tb-create-cf-alarm-rules',
@ -51,28 +51,35 @@ import { AlarmSeverityNotificationColors } from "@shared/models/notification.mod
} }
] ]
}) })
export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy { export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator {
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors;
@Input() @Input()
@coerceBoolean()
disabled: boolean; disabled: boolean;
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
createAlarmRulesFormGroup: UntypedFormGroup;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors;
createAlarmRulesFormGroup = this.fb.group({
createAlarmRules: this.fb.array<{severity: AlarmSeverity, alarmRule: AlarmRule}>([])
});
private usedSeverities: AlarmSeverity[] = []; private usedSeverities: AlarmSeverity[] = [];
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder) { constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) {
this.createAlarmRulesFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateModel());
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -82,20 +89,6 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
ngOnInit() {
this.createAlarmRulesFormGroup = this.fb.group({
createAlarmRules: this.fb.array([])
});
this.createAlarmRulesFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
createAlarmRulesFormArray(): UntypedFormArray { createAlarmRulesFormArray(): UntypedFormArray {
return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; return this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray;
} }
@ -109,7 +102,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
} }
} }
writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void { writeValue(createAlarmRules: Record<AlarmSeverity, AlarmRule>): void {
const createAlarmRulesControls: Array<AbstractControl> = []; const createAlarmRulesControls: Array<AbstractControl> = [];
if (createAlarmRules) { if (createAlarmRules) {
Object.keys(createAlarmRules).forEach((severity) => { Object.keys(createAlarmRules).forEach((severity) => {
@ -123,7 +116,9 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
})); }));
}); });
} }
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls), {emitEvent: false}); const formArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
formArray.clear();
createAlarmRulesControls.forEach(c => formArray.push(c));
if (this.disabled) { if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false}); this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else { } else {
@ -136,11 +131,11 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
} }
public removeCreateAlarmRule(index: number) { public removeCreateAlarmRule(index: number) {
(this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray).removeAt(index); (this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index);
} }
public addCreateAlarmRule() { public addCreateAlarmRule() {
const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray; const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
createAlarmRulesArray.push(this.fb.group({ createAlarmRulesArray.push(this.fb.group({
severity: [this.getFirstUnusedSeverity(), Validators.required], severity: [this.getFirstUnusedSeverity(), Validators.required],
alarmRule: [null, Validators.required] alarmRule: [null, Validators.required]
@ -176,15 +171,15 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
private updateUsedSeverities() { private updateUsedSeverities() {
this.usedSeverities = []; this.usedSeverities = [];
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
value.forEach((rule, index) => { value.forEach((rule, index) => {
this.usedSeverities[index] = AlarmSeverity[rule.severity]; this.usedSeverities[index] = AlarmSeverity[rule.severity];
}); });
} }
private updateModel() { private updateModel() {
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value; const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {}; const createAlarmRules = {} as Record<AlarmSeverity, AlarmRule>;
value.forEach(v => createAlarmRules[v.severity] = v.alarmRule); value.forEach(v => createAlarmRules[v.severity] = v.alarmRule);
this.updateUsedSeverities(); this.updateUsedSeverities();
this.propagateChange(createAlarmRules); this.propagateChange(createAlarmRules);

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

@ -16,9 +16,8 @@
--> -->
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;"> <form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar color="primary"> <mat-toolbar class="justify-between" color="primary">
<h2 translate>filter.complex-filter</h2> <h2 translate>filter.complex-filter</h2>
<span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
@ -26,33 +25,30 @@
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col"> <mat-form-field class="mat-block" appearance="outline">
<mat-form-field class="mat-block" appearance="outline"> <mat-label translate>filter.operation.operation</mat-label>
<mat-label translate>filter.operation.operation</mat-label> <mat-select required formControlName="operation">
<mat-select required formControlName="operation"> <mat-option *ngFor="let operation of complexOperations" [value]="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation"> {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} </mat-option>
</mat-option> </mat-select>
</mat-select> </mat-form-field>
</mat-form-field> <tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType" [arguments]="arguments"
[arguments]="arguments" [operation]="complexFilterFormGroup.get('operation').value"
[operation]="complexFilterFormGroup.get('operation').value" formControlName="predicates">
formControlName="predicates"> </tb-alarm-rule-filter-predicate-list>
</tb-alarm-rule-filter-predicate-list>
</fieldset>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" (click)="cancel()"
cdkFocusInitial> cdkFocusInitial>
{{'action.cancel' | translate }} {{'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty"> [disabled]="complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }} {{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
</div> </div>

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

@ -14,19 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/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';
import { import { FormBuilder, Validators } from '@angular/forms';
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
Validators
} from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { import {
@ -35,8 +28,7 @@ import {
EntityKeyValueType, EntityKeyValueType,
FilterPredicateType FilterPredicateType
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export interface AlarmRuleComplexFilterPredicateDialogData { export interface AlarmRuleComplexFilterPredicateDialogData {
@ -54,10 +46,14 @@ export interface AlarmRuleComplexFilterPredicateDialogData {
}) })
export class AlarmRuleComplexFilterPredicateDialogComponent extends export class AlarmRuleComplexFilterPredicateDialogComponent extends
DialogComponent<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate> DialogComponent<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate> {
implements OnInit, ErrorStateMatcher {
complexFilterFormGroup: FormGroup<FormControlsFrom<ComplexAlarmRuleFilterPredicate>>; complexFilterFormGroup = this.fb.group(
{
operation: [ComplexOperation.AND, [Validators.required]],
predicates: this.fb.control<AlarmRuleFilterPredicate[] | null>(null, Validators.required)
}
);
complexOperations = Object.keys(ComplexOperation); complexOperations = Object.keys(ComplexOperation);
complexOperationEnum = ComplexOperation; complexOperationEnum = ComplexOperation;
@ -65,35 +61,18 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
isAdd: boolean; isAdd: boolean;
submitted = false;
arguments = this.data.arguments; arguments = this.data.arguments;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate>, public dialogRef: MatDialogRef<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.isAdd = this.data.isAdd; this.isAdd = this.data.isAdd;
this.complexFilterFormGroup = this.fb.group( this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false});
{
operation: [this.data.complexPredicate.operation, [Validators.required]],
predicates: [this.data.complexPredicate.predicates, [Validators.required]]
}
);
}
ngOnInit(): void {
}
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
} }
cancel(): void { cancel(): void {
@ -101,7 +80,6 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
} }
save(): void { save(): void {
this.submitted = true;
if (this.complexFilterFormGroup.valid) { if (this.complexFilterFormGroup.valid) {
const predicate: ComplexAlarmRuleFilterPredicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate; const predicate: ComplexAlarmRuleFilterPredicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
predicate.type = FilterPredicateType.COMPLEX; predicate.type = FilterPredicateType.COMPLEX;

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

@ -16,9 +16,8 @@
--> -->
<form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 1200px;"> <form [formGroup]="filterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar color="primary"> <mat-toolbar class="justify-between" color="primary">
<h2>{{(data.isAdd ? 'alarm-rule.add-filter' : ('alarm-rule.edit-filter')) | translate}}</h2> <h2>{{(data.isAdd ? 'alarm-rule.add-filter' : 'alarm-rule.edit-filter') | translate}}</h2>
<span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
@ -26,7 +25,7 @@
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="tb-form-panel no-border no-padding"> <section class="tb-form-panel no-border no-padding">
<section class="tb-form-panel"> <section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.general' | translate }}</div> <div class="tb-form-panel-title">{{ 'alarm-rule.general' | translate }}</div>
<div class="flex flex-row gap-2 xs:flex-col"> <div class="flex flex-row gap-2 xs:flex-col">
@ -81,20 +80,18 @@
formControlName="predicates"> formControlName="predicates">
</tb-alarm-rule-filter-predicate-list> </tb-alarm-rule-filter-predicate-list>
</section> </section>
</section>
</fieldset>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" (click)="cancel()"
cdkFocusInitial> cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty"> [disabled]="filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} {{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
</div> </div>

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

@ -14,19 +14,11 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnDestroy, SkipSelf } from '@angular/core'; import { Component, DestroyRef, Inject } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/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';
import { import { FormBuilder, FormGroup, Validators } from '@angular/forms';
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
Validators
} from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { import {
@ -37,11 +29,10 @@
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AlarmRuleFilter, AlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models"; import { AlarmRuleFilter, AlarmRuleFilterPredicate } 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";
export interface AlarmRuleFilterDialogData { export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter; filter: AlarmRuleFilter;
@ -53,13 +44,10 @@
@Component({ @Component({
selector: 'tb-alarm-rule-filter-dialog', selector: 'tb-alarm-rule-filter-dialog',
templateUrl: './alarm-rule-filter-dialog.component.html', templateUrl: './alarm-rule-filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleFilterDialogComponent}], providers: [],
styleUrls: ['./alarm-rule-filter-dialog.component.scss'] styleUrls: ['./alarm-rule-filter-dialog.component.scss']
}) })
export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFilterDialogComponent, AlarmRuleFilter> export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFilterDialogComponent, AlarmRuleFilter> {
implements OnDestroy, ErrorStateMatcher {
private destroy$ = new Subject<void>();
filterFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilter>>; filterFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilter>>;
@ -73,22 +61,21 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
ComplexOperation = ComplexOperation; ComplexOperation = ComplexOperation;
submitted = false;
searchText = '';
arguments = this.data.arguments; arguments = this.data.arguments;
argumentsList: Array<string>;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleFilterDialogComponent, AlarmRuleFilter>, public dialogRef: MatDialogRef<AlarmRuleFilterDialogComponent, AlarmRuleFilter>,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private fb: UntypedFormBuilder) { private fb: FormBuilder,
private destroyRef: DestroyRef) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.argumentsList = this.arguments ? Object.keys(this.arguments) : [];
this.filterFormGroup = this.fb.group( this.filterFormGroup = this.fb.group(
{ {
argument: [this.data.filter.argument, [Validators.required]], argument: [this.data.filter.argument, [Validators.required]],
@ -98,7 +85,7 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
} }
); );
this.filterFormGroup.get('valueType').valueChanges.pipe( this.filterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe((valueType: EntityKeyValueType) => { ).subscribe((valueType: EntityKeyValueType) => {
const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType; const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType;
const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value; const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value;
@ -119,35 +106,15 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
}); });
} }
ngOnDestroy() {
super.ngOnDestroy();
this.destroy$.next();
this.destroy$.complete();
}
argumentInUse(argument: string): boolean { argumentInUse(argument: string): boolean {
return this.data.usedArguments.includes(argument); return this.data.usedArguments.includes(argument);
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid && this.submitted);
return originalErrorState || customErrorState;
}
cancel(): void { cancel(): void {
this.dialogRef.close(null); this.dialogRef.close(null);
} }
save(): void { save(): void {
this.submitted = true; this.dialogRef.close(this.filterFormGroup.value as AlarmRuleFilter);
if (this.filterFormGroup.valid) {
const keyFilter: AlarmRuleFilter = this.filterFormGroup.getRawValue();
this.dialogRef.close(keyFilter);
}
}
get argumentsList(): Array<string> {
return this.arguments ? Object.keys(this.arguments) : [];
} }
} }

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

@ -15,67 +15,65 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-form-panel no-padding no-gap stroked" [formGroup]="filterListFormGroup"> <section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<div class="flex flex-row"> <div class="flex flex-row">
<span class="max-w-8% flex-full"></span> <span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start"> <div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label> <label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label> <label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span style="min-width: 96px;">&nbsp;</span> <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" style="max-height: 76px;" <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">{{ filterControl.value?.argument }}</div>
<div class="flex-1">{{ FilterPredicateTypeTranslationMap.get(filterControl.value?.predicates[0]?.type) | translate }}</div>
<button mat-icon-button color="primary"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>{{'edit'}}</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div> </div>
@if (index) { }
<mat-divider></mat-divider> </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?.predicates[0]?.type) | translate }}</div>
<button mat-icon-button color="primary"
type="button"
(click)="editFilter(index)"
matTooltip="{{ 'filter.edit-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>{{'edit'}}</mat-icon>
</button>
<button mat-icon-button color="primary"
type="button"
(click)="removeFilter(index)"
matTooltip="{{ 'filter.remove-key-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div> </div>
</div>
@if (index) {
<mat-divider></mat-divider>
} }
<span [class.!hidden]="!!filtersFormArray.length"
class="no-data-found flex items-center justify-center"
translate>
filter.no-key-filters
</span>
</div> </div>
}
<span [class.!hidden]="!!filtersFormArray.length"
class="no-data-found flex items-center justify-center"
translate>
filter.no-key-filters
</span>
</div>
</section> </section>
<div style="margin-top: 16px;"> <button mat-button mat-raised-button color="primary"
<button mat-button mat-raised-button color="primary" (click)="addFilter()"
(click)="addFilter()" type="button"
type="button" matTooltip="{{ 'alarm-rule.add-filter' | translate }}"
matTooltip="{{ 'alarm-rule.add-filter' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> {{ 'alarm-rule.add-filter' | translate }}
{{ 'alarm-rule.add-filter' | translate }} </button>
</button>
</div>

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

@ -14,21 +14,19 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormControl, FormArray,
FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, Subject } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
ComplexOperation, ComplexOperation,
complexOperationTranslationMap, complexOperationTranslationMap,
@ -36,14 +34,13 @@ import {
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { takeUntil } from 'rxjs/operators';
import { 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, FilterPredicateTypeTranslationMap } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { UtilsService } from "@core/services/utils.service"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'tb-alarm-rule-filter-list', selector: 'tb-alarm-rule-filter-list',
@ -62,45 +59,34 @@ import { UtilsService } from "@core/services/utils.service";
} }
] ]
}) })
export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator {
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
@Input() operation: ComplexOperation = ComplexOperation.AND; @Input()
operation: ComplexOperation = ComplexOperation.AND;
filterListFormGroup: UntypedFormGroup; filterListFormGroup = this.fb.group({
filtersControl: FormControl; filters: this.fb.array([])
});
complexOperationTranslationMap = complexOperationTranslationMap; complexOperationTranslationMap = complexOperationTranslationMap;
FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap FilterPredicateTypeTranslationMap = FilterPredicateTypeTranslationMap
private destroy$ = new Subject<void>(); private propagateChange = (v: any) => { };
private propagateChange = null;
constructor(private fb: UntypedFormBuilder,
private utils: UtilsService,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.filterListFormGroup = this.fb.group({
filters: this.fb.array([])
});
this.filtersControl = this.fb.control(null);
constructor(private fb: FormBuilder,
private dialog: MatDialog,
private destroyRef: DestroyRef) {
this.filterListFormGroup.valueChanges.pipe( this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateModel()); ).subscribe(() => this.updateModel());
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get filtersFormArray(): UntypedFormArray { get filtersFormArray(): FormArray {
return this.filterListFormGroup.get('filters') as UntypedFormArray; return this.filterListFormGroup.get('filters') as FormArray;
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -111,32 +97,28 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.filterListFormGroup.valid && this.filtersControl.valid && this.filterListFormGroup.get('filters').value?.length ? null : { return this.filterListFormGroup.valid && this.filterListFormGroup.get('filters').value?.length ? null : {
filterList: {valid: false} filterList: {valid: false}
}; };
} }
writeValue(filters: Array<AlarmRuleFilter>): void { writeValue(filters: Array<AlarmRuleFilter>): void {
if (filters?.length === this.filtersFormArray?.length) { const keyFilterControls: Array<AbstractControl> = [];
this.filtersFormArray.patchValue(filters, {emitEvent: false}); if (filters) {
} else { for (const filter of filters) {
const keyFilterControls: Array<AbstractControl> = []; keyFilterControls.push(this.fb.control(filter, [Validators.required]));
if (filters) {
for (const filter of filters) {
keyFilterControls.push(this.fb.control(filter, [Validators.required]));
}
} }
this.filterListFormGroup.setControl('filters', this.fb.array(keyFilterControls), {emitEvent: false});
} }
this.filtersControl.patchValue(filters, {emitEvent: false}); this.filtersFormArray.clear();
keyFilterControls.forEach(c => this.filtersFormArray.push(c));
} }
public removeFilter(index: number) { public removeFilter(index: number) {
(this.filterListFormGroup.get('filters') as UntypedFormArray).removeAt(index); (this.filterListFormGroup.get('filters') as FormArray).removeAt(index);
} }
public addFilter() { public addFilter() {
const filtersFormArray = this.filterListFormGroup.get('filters') as UntypedFormArray; const filtersFormArray = this.filterListFormGroup.get('filters') as FormArray;
this.openFilterDialog(null).subscribe(result => { this.openFilterDialog(null).subscribe(result => {
if (result) { if (result) {
filtersFormArray.push(this.fb.control(result, [Validators.required])); filtersFormArray.push(this.fb.control(result, [Validators.required]));
@ -146,17 +128,17 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
public editFilter(index: number) { public editFilter(index: number) {
const filter: AlarmRuleFilter = const filter: AlarmRuleFilter =
(this.filterListFormGroup.get('filters') as UntypedFormArray).at(index).value; (this.filterListFormGroup.get('filters') as FormArray).at(index).value;
this.openFilterDialog(filter).subscribe(result => { this.openFilterDialog(filter).subscribe(result => {
if (result) { if (result) {
(this.filterListFormGroup.get('filters') as UntypedFormArray).at(index).patchValue(result); (this.filterListFormGroup.get('filters') as FormArray).at(index).patchValue(result);
} }
}); });
} }
private openFilterDialog(filter?: AlarmRuleFilter): Observable<AlarmRuleFilter> { private openFilterDialog(filter?: AlarmRuleFilter): Observable<AlarmRuleFilter> {
const isAdd = !filter; const isAdd = !filter;
if (!filter) { if (isAdd) {
filter = { filter = {
argument: null, argument: null,
valueType: EntityKeyValueType.STRING, valueType: EntityKeyValueType.STRING,
@ -178,18 +160,13 @@ export class AlarmRuleFilterListComponent implements ControlValueAccessor, Valid
} }
get getUsedArguments(): Array<string> { get getUsedArguments(): Array<string> {
const filters = this.filterListFormGroup.get('filters').value ?? []; const filters = this.filterListFormGroup.get('filters').value as Array<AlarmRuleFilter>;
return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : filters; return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : [];
} }
private updateModel() { private updateModel() {
const filters: Array<AlarmRuleFilter> = this.filterListFormGroup.getRawValue().filters; const filters = this.filterListFormGroup.value.filters as Array<AlarmRuleFilter>;
this.filtersControl.patchValue(filters, {emitEvent: false}); this.propagateChange(filters);
if (filters.length) {
this.propagateChange(filters);
} else {
this.propagateChange(null);
}
} }
} }

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<section class="flex flex-col" [formGroup]="filterListFormGroup"> <section class="flex flex-col gap-4" [formGroup]="filterListFormGroup">
<div class="tb-form-panel no-padding no-gap stroked"> <div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row"> <div class="flex flex-row">
<div class="flex flex-full flex-row items-center justify-start gap-2"> <div class="flex flex-full flex-row items-center justify-start gap-2">
@ -24,15 +24,14 @@
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</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> <label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label>
</div> </div>
<span [class.!hidden]="disabled" style="min-width: 40px;">&nbsp;</span> <span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div> </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">
formArrayName="predicates">
@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>
@ -60,25 +59,27 @@
} }
<span [class.!hidden]="!!predicatesFormArray.length" <span [class.!hidden]="!!predicatesFormArray.length"
[class.disabled]="disabled" [class.disabled]="disabled"
class="no-data-found flex items-center justify-center" translate>filter.no-filters</span> class="no-data-found flex items-center justify-center" translate>
filter.no-filters
</span>
</div> </div>
</div> </div>
<div style="margin-top: 16px;" class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled" [class.!hidden]="disabled"
(click)="addPredicate(false)" (click)="addPredicate(false)"
type="button" type="button"
matTooltip="{{ 'filter.add-filter' | translate }}" matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled" [class.!hidden]="disabled"
(click)="addPredicate(true)" (click)="addPredicate(true)"
type="button" type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}" matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
{{ 'filter.add-complex' | translate }} {{ 'filter.add-complex' | translate }}
</button> </button>
</div> </div>
</section> </section>

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

@ -14,20 +14,19 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input } from '@angular/core';
import { import {
AbstractControl, AbstractControl,
ControlValueAccessor, ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of, Subject } from 'rxjs'; import { Observable, of } from 'rxjs';
import { import {
BooleanOperation, BooleanOperation,
ComplexOperation, ComplexOperation,
@ -39,9 +38,7 @@ import {
StringOperation StringOperation
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { map, takeUntil } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { ComponentType } from '@angular/cdk/portal';
import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens';
import { import {
AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleComplexFilterPredicateDialogData AlarmRuleComplexFilterPredicateDialogData
@ -52,6 +49,7 @@ import {
ComplexAlarmRuleFilterPredicate ComplexAlarmRuleFilterPredicate
} 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 { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@Component({ @Component({
selector: 'tb-alarm-rule-filter-predicate-list', selector: 'tb-alarm-rule-filter-predicate-list',
@ -70,7 +68,7 @@ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"
} }
] ]
}) })
export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy { export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -80,36 +78,26 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
@Input() arguments: Record<string, CalculatedFieldArgument>; @Input() arguments: Record<string, CalculatedFieldArgument>;
filterListFormGroup: UntypedFormGroup; filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
valueTypeEnum = EntityKeyValueType; valueTypeEnum = EntityKeyValueType;
complexOperationTranslations = complexOperationTranslationMap; complexOperationTranslations = complexOperationTranslationMap;
private destroy$ = new Subject<void>(); private propagateChange= (v: any) => { };
private propagateChange = null;
constructor(private fb: UntypedFormBuilder, constructor(private fb: FormBuilder,
@Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType<any>, private dialog: MatDialog,
private dialog: MatDialog) { private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
this.filterListFormGroup.valueChanges.pipe( this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$) takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateModel()); ).subscribe(() => this.updateModel());
} }
ngOnDestroy() { get predicatesFormArray(): FormArray {
this.destroy$.next(); return this.filterListFormGroup.get('predicates') as FormArray;
this.destroy$.complete();
}
get predicatesFormArray(): UntypedFormArray {
return this.filterListFormGroup.get('predicates') as UntypedFormArray;
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -135,22 +123,14 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
} }
writeValue(predicates: Array<AlarmRulePredicateInfo>): void { writeValue(predicates: Array<AlarmRulePredicateInfo>): void {
if (predicates?.length === this.predicatesFormArray.length) { const predicateControls: Array<AbstractControl> = [];
this.predicatesFormArray.patchValue(predicates, {emitEvent: false}); if (predicates) {
} else { for (const predicate of predicates) {
const predicateControls: Array<AbstractControl> = []; predicateControls.push(this.fb.control(predicate, [Validators.required]));
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.filterListFormGroup.setControl('predicates', this.fb.array(predicateControls), {emitEvent: false});
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
} }
} }
this.predicatesFormArray.clear();
predicateControls.forEach(predicate => this.predicatesFormArray.push(predicate));
} }
public removePredicate(index: number) { public removePredicate(index: number) {
@ -158,7 +138,7 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
} }
public addPredicate(complex: boolean) { public addPredicate(complex: boolean) {
const predicatesFormArray = this.filterListFormGroup.get('predicates') as UntypedFormArray; const predicatesFormArray = this.filterListFormGroup.get('predicates') as FormArray;
const predicate = this.createDefaultFilterPredicate(this.valueType, complex); const predicate = this.createDefaultFilterPredicate(this.valueType, complex);
let observable: Observable<AlarmRuleFilterPredicate>; let observable: Observable<AlarmRuleFilterPredicate>;
if (complex) { if (complex) {
@ -217,23 +197,11 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
arguments: this.arguments, arguments: this.arguments,
} }
}).afterClosed().pipe( }).afterClosed().pipe(
map((result) => { map(result => result)
if (result) {
predicate = result;
return predicate;
} else {
return null;
}
})
); );
} }
private updateModel() { private updateModel() {
const predicates: Array<AlarmRulePredicateInfo> = this.filterListFormGroup.getRawValue().predicates; this.propagateChange(this.filterListFormGroup.get('predicates').value);
if (predicates.length) {
this.propagateChange(predicates);
} else {
this.propagateChange(null);
}
} }
} }

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

@ -18,19 +18,12 @@
<div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueFormGroup"> <div class="tb-form-panel no-border no-gap no-padding" [formGroup]="filterPredicateValueFormGroup">
<div class="tb-form-row no-padding no-border column-xs"> <div class="tb-form-row no-padding no-border column-xs">
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select [ngModel]="mode" <mat-select [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}">
(ngModelChange)="onModeChange($event)" <mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option>
[ngModelOptions]="{ standalone: true }" <mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option>
placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="'static'">
{{'alarm-rule.static' | translate}}
</mat-option>
<mat-option [value]="'dynamic'">
{{'alarm-rule.dynamic' | translate}}
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@if (mode === 'static') { @if (!dynamicModeControl.value) {
@switch (valueType) { @switch (valueType) {
@case (valueTypeEnum.STRING) { @case (valueTypeEnum.STRING) {
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">

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

@ -17,10 +17,10 @@
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder,
FormGroup, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder,
ValidationErrors, ValidationErrors,
Validator, Validator,
ValidatorFn, ValidatorFn,
@ -30,7 +30,6 @@ import { EntityKeyValueType } from '@shared/models/query/query.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmRuleValue } from "@shared/models/alarm-rule.models"; import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
import { isDefinedAndNotNull } from "@core/utils";
import { FormControlsFrom } from "@shared/models/tenant.model"; import { FormControlsFrom } from "@shared/models/tenant.model";
@Component({ @Component({
@ -62,16 +61,18 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
filterPredicateValueFormGroup: FormGroup<FormControlsFrom<AlarmRuleValue<string | number | boolean>>>; filterPredicateValueFormGroup: FormGroup<FormControlsFrom<AlarmRuleValue<string | number | boolean>>>;
mode: 'static' | 'dynamic' = 'static'; dynamicModeControl = this.fb.control(false);
private propagateChange = null; argumentsList: Array<string>;
private propagateChangePending = false;
constructor(private fb: UntypedFormBuilder, private propagateChange= (v: any) => { };
constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
} }
ngOnInit(): void { ngOnInit(): void {
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
let defaultValue: string | number | boolean; let defaultValue: string | number | boolean;
let defaultValueValidators: ValidatorFn[]; let defaultValueValidators: ValidatorFn[];
switch (this.valueType) { switch (this.valueType) {
@ -94,42 +95,30 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
} }
this.filterPredicateValueFormGroup = this.fb.group({ this.filterPredicateValueFormGroup = this.fb.group({
staticValue: [defaultValue, defaultValueValidators], staticValue: [defaultValue, defaultValueValidators],
dynamicValueArgument: [null, Validators.required] dynamicValueArgument: ['', Validators.required]
}); });
this.filterPredicateValueFormGroup.valueChanges.pipe( this.filterPredicateValueFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
this.updateModel(); this.updateModel();
}); });
this.dynamicModeControl.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => this.updateValueModeValidators(value))
} }
private updateValueModeValidators(mode: 'static' | 'dynamic'): void { private updateValueModeValidators(isDynamicMode: boolean): void {
if (mode === 'static') { if (isDynamicMode) {
this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
} else {
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false}); this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({emitEvent: false}); this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({emitEvent: false});
} else {
this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
} }
} }
get argumentsList(): Array<string> {
return this.arguments ? Object.keys(this.arguments): [];
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.updateModel();
}, 0);
}
}
onModeChange(mode: 'static' | 'dynamic') {
this.mode = mode;
this.updateValueModeValidators(mode);
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
@ -142,18 +131,11 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
} }
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void { writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
this.propagateChangePending = false;
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false}); this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
this.mode = isDefinedAndNotNull(predicateValue.dynamicValueArgument) ? 'dynamic' : 'static'; this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length);
this.updateValueModeValidators(this.mode);
} }
private updateModel() { private updateModel() {
const predicateValue: AlarmRuleValue<string | number | boolean> = this.filterPredicateValueFormGroup.value; this.propagateChange(this.filterPredicateValueFormGroup.value);
if (this.propagateChange) {
this.propagateChange(predicateValue);
} else {
this.propagateChangePending = true;
}
} }
} }

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

@ -14,13 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core'; import { Component, DestroyRef, forwardRef, Input } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormGroup, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormBuilder,
ValidationErrors, ValidationErrors,
Validator Validator
} from '@angular/forms'; } from '@angular/forms';
@ -41,7 +40,6 @@ import {
AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleComplexFilterPredicateDialogData AlarmRuleComplexFilterPredicateDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component"; } from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"; import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@Component({ @Component({
@ -61,7 +59,7 @@ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"
} }
] ]
}) })
export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit { export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator {
@Input() @Input()
valueType: EntityKeyValueType; valueType: EntityKeyValueType;
@ -69,7 +67,12 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
@Input() @Input()
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
filterPredicateFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilterPredicate>>; filterPredicateFormGroup = this.fb.group({
operation: [],
ignoreCase: false,
predicates: [],
value: []
});
type: FilterPredicateType; type: FilterPredicateType;
@ -87,20 +90,11 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
booleanOperationEnum = BooleanOperation; booleanOperationEnum = BooleanOperation;
booleanOperationTranslations = booleanOperationTranslationMap; booleanOperationTranslations = booleanOperationTranslationMap;
private propagateChange = null; private propagateChange= (v: any) => { };
constructor(private fb: UntypedFormBuilder, constructor(private fb: FormBuilder,
private dialog: MatDialog, private dialog: MatDialog,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.filterPredicateFormGroup = this.fb.group({
operation: [],
ignoreCase: false,
predicates: [],
value: []
});
this.filterPredicateFormGroup.valueChanges.pipe( this.filterPredicateFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe(() => { ).subscribe(() => {
@ -127,12 +121,7 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
} }
private updateModel() { private updateModel() {
let predicate: AlarmRuleFilterPredicate = null; this.propagateChange({type: this.type, ...this.filterPredicateFormGroup.value});
if (this.filterPredicateFormGroup.valid) {
predicate = this.filterPredicateFormGroup.getRawValue();
predicate.type = this.type;
}
this.propagateChange(predicate);
} }
public openComplexFilterDialog() { public openComplexFilterDialog() {
@ -141,7 +130,7 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
complexPredicate: this.filterPredicateFormGroup.getRawValue() as ComplexAlarmRuleFilterPredicate, complexPredicate: this.filterPredicateFormGroup.value as ComplexAlarmRuleFilterPredicate,
valueType: this.valueType, valueType: this.valueType,
isAdd: false, isAdd: false,
arguments: this.arguments, arguments: this.arguments,
@ -149,8 +138,7 @@ export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor,
}).afterClosed().subscribe( }).afterClosed().subscribe(
(result) => { (result) => {
if (result) { if (result) {
this.filterPredicateFormGroup.patchValue(result, {emitEvent: false}); this.filterPredicateFormGroup.patchValue(result);
this.updateModel();
} }
} }
); );

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

@ -17,7 +17,7 @@
--> -->
<div class="tb-filter-text" <div class="tb-filter-text"
tbTruncateWithTooltip tbTruncateWithTooltip
[class.required]="requiredClass" [class.required]="isRequired"
[class.nowrap]="nowrap" [class.nowrap]="nowrap"
[innerHTML]="filterText"> [innerHTML]="filterText">
</div> </div>

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

@ -86,7 +86,7 @@ export class AlarmRuleFilterTextComponent {
} }
} }
requiredClass = false; isRequired = false;
public filterText: string; public filterText: string;
@ -95,7 +95,7 @@ export class AlarmRuleFilterTextComponent {
} }
private updateFilterText(value: AlarmRuleExpression) { private updateFilterText(value: AlarmRuleExpression) {
this.requiredClass = false; this.isRequired = false;
if (value && (value.expression || value.filters)) { if (value && (value.expression || value.filters)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) { if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation); this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
@ -108,7 +108,7 @@ export class AlarmRuleFilterTextComponent {
} else { } else {
if (this.required) { if (this.required) {
this.filterText = this.addFilterPrompt; this.filterText = this.addFilterPrompt;
this.requiredClass = true; this.isRequired = true;
} else { } else {
this.filterText = this.noFilterText; this.filterText = this.noFilterText;
} }

14
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts

@ -33,7 +33,7 @@ import {
ArgumentEntityTypeTranslations, ArgumentEntityTypeTranslations,
ArgumentType, ArgumentType,
ArgumentTypeTranslations, ArgumentTypeTranslations,
CalculatedFieldArgumentValue, CFArgumentDynamicSourceType, CalculatedFieldArgumentValue,
getCalculatedFieldCurrentEntityFilter getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
@ -144,7 +144,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.argumentFormGroup.patchValue(this.argument, {emitEvent: false}); this.argumentFormGroup.patchValue(this.argument, {emitEvent: false});
this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
this.updateEntityFilter(this.entityType, true); this.updateEntityFilter(this.entityType, true);
this.updatedRefEntityIdState(this.entityType); this.updatedRefEntityIdState(this.entityType, false);
this.toggleByEntityKeyType(this.argument.refEntityKey?.type); this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType(); this.setInitialEntityKeyType();
this.setInitialEntityType(); this.setInitialEntityType();
@ -168,7 +168,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
saveArgument(): void { saveArgument(): void {
const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue; const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue;
if (this.entityType === ArgumentEntityType.Owner) { if (this.entityType === ArgumentEntityType.Owner) {
value.refDynamicSource = CFArgumentDynamicSourceType.CURRENT_OWNER; value.refDynamicSourceConfiguration.type = ArgumentEntityType.Owner;
} else if (this.entityType === ArgumentEntityType.Tenant) { } else if (this.entityType === ArgumentEntityType.Tenant) {
value.refEntityId = new TenantId(this.tenantId) as any; value.refEntityId = new TenantId(this.tenantId) as any;
} }
@ -188,7 +188,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
private updatedArgumentType(): void { private updatedArgumentType(): void {
let argumentType = ArgumentEntityType.Current; let argumentType = ArgumentEntityType.Current;
if (this.argument.refEntityId?.entityType) { if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) {
argumentType = ArgumentEntityType.Owner;
} else if (this.argument.refEntityId?.entityType) {
argumentType = this.argument.refEntityId.entityType; argumentType = this.argument.refEntityId.entityType;
} }
this.argumentType.setValue(argumentType, {emitEvent: false}); this.argumentType.setValue(argumentType, {emitEvent: false});
@ -312,9 +314,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
}; };
} }
private updatedRefEntityIdState(type: ArgumentEntityType): void { private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void {
const isEntityWithId = !!type && type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current; const isEntityWithId = !!type && type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current;
this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable'](); this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']({emitEvent});
if (!isEntityWithId) { if (!isEntityWithId) {
this.entityNameSubject.next(null); this.entityNameSubject.next(null);
} }

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

@ -44,6 +44,8 @@
<div tbTruncateWithTooltip> <div tbTruncateWithTooltip>
@if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) { @if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) {
{{ 'calculated-fields.argument-current-tenant' | translate }} {{ 'calculated-fields.argument-current-tenant' | translate }}
} @else if (argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) {
{{ 'calculated-fields.argument-owner' | translate }}
} @else if (argument.refEntityId?.id) { } @else if (argument.refEntityId?.id) {
{{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }} {{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }}
} @else { } @else {

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

@ -17,7 +17,7 @@
--> -->
<div class="debug-dialog-container flex flex-col xs:h-full"> <div class="debug-dialog-container flex flex-col xs:h-full">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ 'calculated-fields.debugging' | translate}}</h2> <h2>{{ dialogTitle | translate}}</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component.ts

@ -47,11 +47,14 @@ export class CalculatedFieldDebugDialogComponent extends DialogComponent<Calcula
readonly debugEventTypes = DebugEventType; readonly debugEventTypes = DebugEventType;
readonly EventType = EventType; readonly EventType = EventType;
dialogTitle: string;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) { protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.dialogTitle = this.data.value.type === CalculatedFieldType.ALARM ? 'alarm-rule.debugging' : 'calculated-fields.debugging';
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {

7
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.ts

@ -53,6 +53,7 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
@Input({ transform: booleanAttribute }) failuresEnabled = false; @Input({ transform: booleanAttribute }) failuresEnabled = false;
@Input({ transform: booleanAttribute }) allEnabled = false; @Input({ transform: booleanAttribute }) allEnabled = false;
@Input({ transform: booleanAttribute }) isAlarmRule = false;
@Input() entityType: EntityType; @Input() entityType: EntityType;
@Input() allEnabledUntil = 0; @Input() allEnabledUntil = 0;
@Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE; @Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
@ -109,7 +110,11 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
this.onFailuresControl.patchValue(this.failuresEnabled); this.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled); this.debugAllControl.patchValue(this.allEnabled);
this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime(); this.initialAllEnabled = this.allEnabled || this.allEnabledUntil > new Date().getTime();
this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity'; if (this.isAlarmRule) {
this.entityLabel = 'alarm-rule.alarm-rule';
} else {
this.entityLabel = entityTypeTranslations.has(this.entityType) ? entityTypeTranslations.get(this.entityType).type : 'debug-settings.entity';
}
} }
onCancel(): void { onCancel(): void {

1
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings.model.ts

@ -28,6 +28,7 @@ export interface EntityDebugSettingPanelConfig {
maxDebugModeDuration?: number; maxDebugModeDuration?: number;
additionalActionConfig?: AdditionalDebugActionConfig; additionalActionConfig?: AdditionalDebugActionConfig;
entityType: EntityType; entityType: EntityType;
isAlarmRule?: boolean;
} }
onSettingsAppliedFn: (settings: EntityDebugSettings) => void; onSettingsAppliedFn: (settings: EntityDebugSettings) => void;
} }

32
ui-ngx/src/app/modules/home/components/profile/device-profile.component.html

@ -154,21 +154,23 @@
</tb-device-profile-transport-configuration> </tb-device-profile-transport-configuration>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
<mat-expansion-panel [expanded]="false"> @if (entityForm.get('profileData.alarms').value?.length) {
<mat-expansion-panel-header> <mat-expansion-panel [expanded]="false">
<mat-panel-title> <mat-expansion-panel-header>
<div>{{'device-profile.alarm-rules-with-count' | translate: <mat-panel-title>
{count: entityForm.get('profileData.alarms').value ? <div>{{'device-profile.alarm-rules-with-count' | translate:
entityForm.get('profileData.alarms').value.length : 0} }}</div> {count: entityForm.get('profileData.alarms').value ?
</mat-panel-title> entityForm.get('profileData.alarms').value.length : 0} }}</div>
</mat-expansion-panel-header> </mat-panel-title>
<ng-template matExpansionPanelContent> </mat-expansion-panel-header>
<tb-device-profile-alarms <ng-template matExpansionPanelContent>
formControlName="alarms" <tb-device-profile-alarms
[deviceProfileId]="deviceProfileId"> formControlName="alarms"
</tb-device-profile-alarms> [deviceProfileId]="deviceProfileId">
</ng-template> </tb-device-profile-alarms>
</mat-expansion-panel> </ng-template>
</mat-expansion-panel>
}
<mat-expansion-panel [expanded]="true"> <mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title> <mat-panel-title>

16
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -51,22 +51,22 @@
<section class="flex flex-col h-full"> <section class="flex flex-col h-full">
@if (hasOldRules && !isEdit) { @if (hasOldRules && !isEdit) {
<div class="flex flex-row items-center justify-end" style="padding: 16px 16px 0;"> <div class="flex flex-row items-center justify-end" style="padding: 16px 16px 0;">
<tb-toggle-select [(ngModel)]="alarmRulesVersion" appearance="fill"> <tb-toggle-select [(ngModel)]="alarmRulesOldVersion" appearance="fill">
<tb-toggle-option [value]="true">{{ 'alarm-rule.alarm-rules-new' | translate }}</tb-toggle-option> <tb-toggle-option [value]="false">{{ 'alarm-rule.alarm-rules-new' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="false">{{ 'alarm-rule.alarm-rules-old' | translate }}</tb-toggle-option> <tb-toggle-option [value]="true">{{ 'alarm-rule.alarm-rules-old' | translate }}</tb-toggle-option>
</tb-toggle-select> </tb-toggle-select>
</div> </div>
} }
@if (alarmRulesVersion) { @if (alarmRulesOldVersion) {
<div class="relative h-full">
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table>
</div>
} @else {
<div class="mat-padding" [formGroup]="detailsForm" *ngIf="alarmRules.isActive"> <div class="mat-padding" [formGroup]="detailsForm" *ngIf="alarmRules.isActive">
<div formGroupName="profileData"> <div formGroupName="profileData">
<tb-device-profile-alarms formControlName="alarms" [deviceProfileId]="entity.id"></tb-device-profile-alarms> <tb-device-profile-alarms formControlName="alarms" [deviceProfileId]="entity.id"></tb-device-profile-alarms>
</div> </div>
</div> </div>
} @else {
<div class="relative h-full">
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table>
</div>
} }
</section> </section>
</mat-tab> </mat-tab>

4
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts

@ -42,7 +42,7 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
isTransportTypeChanged = false; isTransportTypeChanged = false;
hasOldRules = false; hasOldRules = false;
alarmRulesVersion = false; alarmRulesOldVersion = false;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
@ -61,7 +61,7 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
protected setEntity(entity: DeviceProfile) { protected setEntity(entity: DeviceProfile) {
this.isTransportTypeChanged = false; this.isTransportTypeChanged = false;
this.hasOldRules = !!entity?.profileData?.alarms?.length; this.hasOldRules = !!entity?.profileData?.alarms?.length;
this.alarmRulesVersion = !this.hasOldRules; this.alarmRulesOldVersion = this.isEdit || false;
super.setEntity(entity); super.setEntity(entity);
} }

17
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -32,6 +32,7 @@ import {
import { EntitySearchDirection } from '@shared/models/relation.models'; import { EntitySearchDirection } from '@shared/models/relation.models';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { AlarmRule } from "@shared/models/alarm-rule.models"; import { AlarmRule } from "@shared/models/alarm-rule.models";
import { AlarmSeverity } from "@shared/models/alarm.models";
interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> { interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
entityId: EntityId; entityId: EntityId;
@ -139,11 +140,11 @@ interface BasePropagationConfiguration {
interface CalculatedFieldAlarmRuleConfiguration { interface CalculatedFieldAlarmRuleConfiguration {
type: CalculatedFieldType.ALARM; type: CalculatedFieldType.ALARM;
arguments: Record<string, CalculatedFieldArgument>; arguments: Record<string, CalculatedFieldArgument>;
createRules: {[severity: string]: AlarmRule}; createRules: Record<AlarmSeverity, AlarmRule>;
clearRule?: AlarmRule; clearRule?: AlarmRule;
propagate?: boolean; propagate: boolean;
propagateToOwner?: boolean; propagateToOwner: boolean;
propagateToTenant?: boolean; propagateToTenant: boolean;
propagateRelationTypes?: Array<string>; propagateRelationTypes?: Array<string>;
} }
@ -266,15 +267,11 @@ export const ArgumentTypeTranslations = new Map<ArgumentType, string>(
] ]
) )
export enum CFArgumentDynamicSourceType {
CURRENT_OWNER = 'CURRENT_OWNER'
}
export interface CalculatedFieldArgument { export interface CalculatedFieldArgument {
refEntityKey: RefEntityKey; refEntityKey: RefEntityKey;
defaultValue?: string; defaultValue?: string;
refEntityId?: RefEntityId; refEntityId?: RefEntityId;
refDynamicSource?: CFArgumentDynamicSourceType; refDynamicSourceConfiguration?: RefDynamicSourceConfiguration;
limit?: number; limit?: number;
timeWindow?: number; timeWindow?: number;
} }
@ -338,7 +335,7 @@ export interface CalculatedFieldGeofencing {
} }
export interface RefDynamicSourceConfiguration { export interface RefDynamicSourceConfiguration {
type?: ArgumentEntityType.RelationQuery; type?: ArgumentEntityType.RelationQuery | ArgumentEntityType.Owner;
levels?: Array<RelationPathLevel>; levels?: Array<RelationPathLevel>;
} }

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

@ -1374,7 +1374,8 @@
"alarm-rule-relation-types-list": "Relation types", "alarm-rule-relation-types-list": "Relation types",
"alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.", "alarm-rule-relation-types-list-hint": "Defines relation types to filter the related entities. If not set, the alarm will be propagated to all related entities.",
"propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)", "propagate-alarm-to-owner": "Propagate alarm to entity owner (Customer or Tenant)",
"propagate-alarm-to-tenant": "Propagate alarm to Tenant" "propagate-alarm-to-tenant": "Propagate alarm to Tenant",
"debugging": "Alarm rule debugging"
}, },
"ai-models": { "ai-models": {
"ai-models": "AI models", "ai-models": "AI models",

Loading…
Cancel
Save