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 { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
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>> {
let url = `/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`;
if (type) {
url += `&type=${type}`;
}
return this.http.get<PageData<CalculatedField>>(url, defaultHttpOptionsFromConfig(config));
return this.http.get<PageData<CalculatedField>>(url, createDefaultHttpOptions(type ? {type} : null, config));
}
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.
-->
<form [formGroup]="editDetailsFormGroup" (ngSubmit)="save()" style="width: 800px; max-width: 100%; display: grid; grid-template-rows: min-content minmax(auto, 1fr) min-content;">
<mat-toolbar color="primary">
<form style="width: 800px">
<mat-toolbar class="justify-between" color="primary">
<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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<mat-form-field class="mat-block" appearance="outline">
<textarea matInput formControlName="alarmDetails" rows="5"
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field>
</div>
</fieldset>
<div class="flex flex-1 flex-col">
<mat-form-field class="mat-block" appearance="outline">
<textarea matInput [formControl]="alarmDetailsControl" rows="5"
placeholder="{{ 'alarm-rule.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'alarm-rule.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field>
</div>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button *ngIf="!data.readonly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!data.readonly) {
<button mat-raised-button color="primary"
type="button"
(click)="save()"
[disabled]="alarmDetailsControl.invalid || !alarmDetailsControl.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
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 { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
export interface AlarmRuleDetailsDialogData {
alarmDetails: string;
@ -32,51 +30,31 @@ export interface AlarmRuleDetailsDialogData {
@Component({
selector: 'tb-edit-alarm-details-dialog',
templateUrl: './alarm-rule-details-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleDetailsDialogComponent}],
styleUrls: []
providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string>
implements OnInit, ErrorStateMatcher {
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string> {
alarmDetails = this.data.alarmDetails;
editDetailsFormGroup: UntypedFormGroup;
submitted = false;
alarmDetailsControl: FormControl<string>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDetailsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleDetailsDialogComponent, string>,
private fb: UntypedFormBuilder,
public translate: TranslateService) {
private fb: FormBuilder) {
super(store, router, dialogRef);
this.editDetailsFormGroup = this.fb.group({
alarmDetails: [this.alarmDetails]
});
this.alarmDetailsControl = this.fb.control(this.data.alarmDetails);
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 {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.alarmDetails = this.editDetailsFormGroup.get('alarmDetails').value;
this.dialogRef.close(this.alarmDetails);
this.dialogRef.close(this.alarmDetailsControl.value);
}
}

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

@ -54,7 +54,7 @@
/>
</div>
</div>
<ng-container [formGroup]="configFormGroup">
<ng-container formGroupName="configuration">
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<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);
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({
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
type: [CalculatedFieldType.ALARM],
debugSettings: [],
configuration: this.fb.group({
arguments: this.fb.control({}),
@ -121,22 +120,18 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
const index = keys.indexOf(key);
if (index >= 0) {
keys.splice(index, 1);
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
this.configFormGroup.get('propagateRelationTypes').setValue(keys);
}
}
addRelationType(event: MatChipInputEvent): void {
const input = event.chipInput.inputElement;
let value = event.value;
if ((value || '').trim()) {
value = value.trim();
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value;
if (!keys || keys.indexOf(value) === -1) {
if (!keys) {
keys = [];
}
let value = (event.value ?? '').trim();
if (value) {
let keys: string[] = this.configFormGroup.get('propagateRelationTypes').value ?? [];
if (keys.indexOf(value) === -1) {
keys.push(value);
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
this.configFormGroup.get('propagateRelationTypes').setValue(keys);
}
}
if (input) {
@ -164,8 +159,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}
private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
const { configuration = {}, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, debugSettings, ...value }, {emitEvent: false});
}
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: [
AlarmRuleDialogComponent,
CreateCfAlarmRulesComponent,
CfAlarmRuleComponent,
CfAlarmRuleConditionComponent,
CfAlarmRuleConditionDialogComponent,
CfAlarmScheduleComponent,
CfAlarmScheduleDialogComponent,
AlarmRuleFilterTextComponent,
AlarmRuleFilterListComponent,
AlarmRuleFilterDialogComponent,
AlarmRuleFilterPredicateListComponent,
AlarmRuleFilterPredicateComponent,
AlarmRuleFilterPredicateValueComponent,
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
]
})
export class AlarmRuleModule { }

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

@ -42,7 +42,6 @@ import {
CalculatedFieldAlarmRule,
CalculatedFieldType,
} from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
@ -128,7 +127,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
name: this.translate.instant('action.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
onAction: (event$, entity) => this.exportAlarmRule(event$, entity),
},
{
name: this.translate.instant('entity-view.events'),
@ -177,6 +176,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
debugSettings,
debugConfig: {
entityType: EntityType.CALCULATED_FIELD,
isAlarmRule: true,
additionalActionConfig,
},
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
@ -226,7 +226,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
.subscribe();
}
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
private exportAlarmRule($event: Event, calculatedField: CalculatedField): void {
if ($event) {
$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.
-->
@if (calculatedFieldsTableConfig) {
<tb-entities-table [entitiesTableConfig]="calculatedFieldsTableConfig"></tb-entities-table>
@if (alarmRulesTableConfig) {
<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 {
tb-entities-table {
.mat-drawer-container {
background-color: white;
}
--mat-sidenav-content-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>();
ownerId = input<EntityId>();
calculatedFieldsTableConfig: AlarmRulesTableConfig;
alarmRulesTableConfig: AlarmRulesTableConfig;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
@ -67,7 +67,7 @@ export class AlarmRulesTableComponent {
effect(() => {
if (this.active()) {
this.calculatedFieldsTableConfig = new AlarmRulesTableConfig(
this.alarmRulesTableConfig = new AlarmRulesTableConfig(
this.calculatedFieldsService,
this.translate,
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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div class="tb-form-panel no-padding no-border">
<div class="flex flex-1 flex-col" formGroupName="expression">
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<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-select>
</div>
<tb-alarm-rule-filter-list
[operation]="conditionFormGroup.get('expression.operation').value"
[arguments]="arguments"
formControlName="filters">
</tb-alarm-rule-filter-list>
<div class="tb-form-panel no-padding no-border">
<div class="flex flex-1 flex-col" formGroupName="expression">
@if (conditionFormGroup.get('expression.type').value === AlarmRuleExpressionType.SIMPLE) {
<div class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.argument-filters' | translate }}</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<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-select>
</div>
<tb-alarm-rule-filter-list
[operation]="conditionFormGroup.get('expression.operation').value"
[arguments]="arguments"
formControlName="filters">
</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>
} @else {
<div class="tb-form-panel no-gap">
<div class="tb-form-panel-title tb-required">
{{ 'alarm-rule.script' | translate }}
<tb-js-func formControlName="expression"
functionName="expression"
[disableUndefinedCheck]="true"
[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>
<tb-js-func formControlName="expression"
functionName="expression"
[disableUndefinedCheck]="true"
[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>
</tb-js-func>
</tb-js-func>
</div>
}
</div>
<section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label>
<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 [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>
<section class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.condition-settings' | translate }}</div>
<mat-form-field class="mat-block" hideRequiredMarker appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.condition-type</mat-label>
<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 class="flex flex-row gap-2 xs:flex-col">
<div class="flex-1" [class.!hidden]="durationDynamicModeControl.value">
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!durationDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'value'}"></ng-container>
</div>
<div class="flex flex-row gap-2 xs:flex-col">
<div class="flex-1" [class.!hidden]="durationDynamicMode">
<ng-container *ngTemplateOutlet="staticValueTemplate; context:{type: AlarmConditionType.DURATION, groupName: 'value'}"></ng-container>
</div>
<div class="flex-1" [class.!hidden]="!durationDynamicMode">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'value'}"></ng-container>
</div>
<div class="max-w-23% flex-full xs:max-w-full">
<mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="unit" required
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ 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 class="max-w-23% flex-full xs:max-w-full">
<mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="unit" required
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ 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>
} @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) {
<div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
<tb-toggle-select [ngModel]="repeatingDynamicMode"
[ngModelOptions]="{standalone: true}"
(ngModelChange)="toggleDynamicMode(AlarmConditionType.REPEATING)"
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>
} @else if (conditionFormGroup.get('type').value == AlarmConditionType.REPEATING) {
<div class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.value' | translate }}</div>
<tb-toggle-select [formControl]="repeatingDynamicModeControl" 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 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 class="flex flex-1 flex-row gap-2">
<div class="flex-1" [class.!hidden]="repeatingDynamicMode">
<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 class="flex-1" [class.!hidden]="!repeatingDynamicModeControl.value">
<ng-container *ngTemplateOutlet="dynamicValueTemplate; context:{groupName: 'count'}"></ng-container>
</div>
</div>
}
</section>
</div>
</fieldset>
</div>
}
</section>
</div>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
<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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
Validators
} from '@angular/forms';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { TimeUnit, timeUnitTranslationMap } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from "@shared/models/rule-node.models";
@ -47,7 +38,6 @@ import {
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
@ -58,11 +48,10 @@ export interface CfAlarmRuleConditionDialogData {
@Component({
selector: 'tb-cf-alarm-rule-condition-dialog',
templateUrl: './cf-alarm-rule-condition-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmRuleConditionDialogComponent}],
providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>
implements OnInit, ErrorStateMatcher {
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition> {
AlarmRuleExpressionType = AlarmRuleExpressionType;
@ -74,9 +63,24 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
readonly = this.data.readonly;
condition = this.data.condition;
conditionFormGroup: FormGroup<FormControlsFrom<AlarmRuleCondition>>;
submitted = false;
conditionFormGroup = this.fb.group({
expression: this.fb.group({
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;
@ -85,8 +89,8 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
defaultValueRangeError = '';
defaultValuePatternError = '';
durationDynamicMode = !!this.condition?.value?.dynamicValueArgument;
repeatingDynamicMode = !!this.condition?.count?.dynamicValueArgument;
durationDynamicModeControl = this.fb.control<boolean>(false);
repeatingDynamicModeControl = this.fb.control<boolean>(false);
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
@ -96,38 +100,41 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
argumentsHighlightRules: AceHighlightRules;
arguments = this.data.arguments;
argumentsList: Array<string>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CfAlarmRuleConditionDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>,
private fb: UntypedFormBuilder,
public translate: TranslateService) {
private fb: FormBuilder) {
super(store, router, dialogRef);
this.functionArgs = ['ctx', ...Object.keys(this.data.arguments)];
this.argumentsEditorCompleter = getCalculatedFieldArgumentsEditorCompleter(this.data.arguments);
this.argumentsHighlightRules = getCalculatedFieldArgumentsHighlights(this.data.arguments);
this.conditionFormGroup = this.fb.group({
expression: this.fb.group({
type: [this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE],
expression: [this.condition?.expression?.expression ?? null, [Validators.required]],
operation: [this.condition?.expression?.operation ?? ComplexOperation.AND],
filters: [this.condition?.expression?.filters],
}),
type: [this.condition?.type ?? AlarmRuleConditionType.SIMPLE, Validators.required],
unit: [this.condition?.unit ?? TimeUnit.SECONDS, Validators.required],
value: this.fb.group({
staticValue: [this.condition?.value?.staticValue ?? null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
dynamicValueArgument: [this.condition?.value?.dynamicValueArgument ?? null, Validators.required],
}),
count: this.fb.group({
staticValue: [this.condition?.count?.staticValue ?? null, [Validators.required, Validators.min(1), Validators.max(2147483647), Validators.pattern('[0-9]*')]],
dynamicValueArgument: [this.condition?.count?.dynamicValueArgument ?? null, Validators.required],
}),
});
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.conditionFormGroup.patchValue({
expression: {
type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
expression: this.condition?.expression?.expression ?? null,
filters: this.condition?.expression?.filters ?? [],
operation: this.condition?.expression?.operation ?? ComplexOperation.AND
},
type: this.condition?.type ?? AlarmRuleConditionType.SIMPLE,
unit: this.condition?.unit ?? TimeUnit.SECONDS,
value: {
staticValue: this.condition?.value?.staticValue,
dynamicValueArgument: this.condition?.value?.dynamicValueArgument,
},
count: {
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(
takeUntilDestroyed()
@ -142,93 +149,70 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
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.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) {
const control = type === AlarmRuleConditionType.DURATION ? 'value' : 'count';
const control = this.conditionFormGroup.get(type === AlarmRuleConditionType.DURATION ? 'value' : 'count');
if (dynamicValue) {
this.conditionFormGroup.get(`${control}.staticValue`).disable({emitEvent: false});
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).enable({emitEvent: false});
control.get('staticValue').disable({emitEvent: false});
control.get('dynamicValueArgument').enable({emitEvent: false});
} else {
this.conditionFormGroup.get(`${control}.staticValue`).enable({emitEvent: false});
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).disable({emitEvent: false});
control.get('staticValue').enable({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') {
if (type === 'SIMPLE') {
this.conditionFormGroup.get(`expression.expression`).disable();
this.conditionFormGroup.get(`expression.filters`).enable();
this.conditionFormGroup.get(`expression.expression`).disable({emitEvent: false});
this.conditionFormGroup.get(`expression.filters`).enable({emitEvent: false});
} else {
this.conditionFormGroup.get(`expression.expression`).enable();
this.conditionFormGroup.get(`expression.filters`).disable();
this.conditionFormGroup.get(`expression.expression`).enable({emitEvent: false});
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) {
switch (type) {
case AlarmRuleConditionType.DURATION:
this.conditionFormGroup.get('unit').enable();
this.conditionFormGroup.get('value').enable();
this.conditionFormGroup.get('count').disable();
this.updateStaticValueValidator(type, this.durationDynamicMode);
this.conditionFormGroup.get('unit').enable({emitEvent: false});
this.conditionFormGroup.get('value').enable({emitEvent: false});
this.conditionFormGroup.get('count').disable({emitEvent: false});
this.updateStaticValueValidator(type, this.durationDynamicModeControl.value);
this.defaultValuePlaceholder = 'alarm-rule.condition-duration-value';
this.defaultValueRequiredError = 'alarm-rule.condition-duration-value-required';
this.defaultValueRangeError = 'alarm-rule.condition-duration-value-range';
this.defaultValuePatternError = 'alarm-rule.condition-duration-value-pattern';
break;
case AlarmRuleConditionType.REPEATING:
this.conditionFormGroup.get('count').enable();
this.conditionFormGroup.get('value').disable();
this.conditionFormGroup.get('unit').disable();
this.updateStaticValueValidator(type, this.repeatingDynamicMode);
this.conditionFormGroup.get('count').enable({emitEvent: false});
this.conditionFormGroup.get('value').disable({emitEvent: false});
this.conditionFormGroup.get('unit').disable({emitEvent: false});
this.updateStaticValueValidator(type, this.repeatingDynamicModeControl.value);
this.defaultValuePlaceholder = 'alarm-rule.condition-repeating-value';
this.defaultValueRequiredError = 'alarm-rule.condition-repeating-value-required';
this.defaultValueRangeError = 'alarm-rule.condition-repeating-value-range';
this.defaultValuePatternError = 'alarm-rule.condition-repeating-value-pattern';
break;
case AlarmRuleConditionType.SIMPLE:
this.conditionFormGroup.get('value').disable();
this.conditionFormGroup.get('count').disable();
this.conditionFormGroup.get('unit').disable();
this.conditionFormGroup.get('value').disable({emitEvent: false});
this.conditionFormGroup.get('count').disable({emitEvent: false});
this.conditionFormGroup.get('unit').disable({emitEvent: false});
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 {
@ -236,9 +220,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
}
save(): void {
this.submitted = true;
this.condition = this.conditionFormGroup.value as AlarmRuleCondition;
this.dialogRef.close(this.condition);
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
}
}

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

@ -14,12 +14,12 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
Validator,
Validators
@ -40,6 +40,7 @@ import {
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleExpressionType,
AlarmRuleSchedule,
AlarmRuleScheduleType
} from "@shared/models/alarm-rule.models";
@ -48,6 +49,7 @@ import {
AlarmRuleScheduleDialogData,
CfAlarmScheduleDialogComponent
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({
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()
@coerceBoolean()
disabled: boolean;
@Input()
@ -76,7 +79,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'],
expression: [null, Validators.required],
expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required],
schedule: [null],
});
@ -89,7 +92,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder,
private fb: FormBuilder,
private cd: ChangeDetectorRef,
private translate: TranslateService) {
}
@ -101,9 +104,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
registerOnTouched(fn: any): void {
}
ngOnInit() {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
@ -119,7 +119,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
}
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) {
@ -147,7 +147,6 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
if (result) {
this.modelValue = {...this.modelValue, ...result};
this.updateModel();
this.updateSpecText();
this.cd.detectChanges();
}
});
@ -233,8 +232,7 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnIn
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue.schedule = result;
this.propagateChange(this.modelValue);
this.updateScheduleText();
this.updateModel();
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.
*/
:host {
min-width: 0;
.row {
margin-top: 1em;
}
.tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px;
&.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 {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
Validator,
Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { isDefinedAndNotNull } from '@core/utils';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -35,6 +34,7 @@ import {
AlarmRuleDetailsDialogComponent,
AlarmRuleDetailsDialogData
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({
selector: 'tb-cf-alarm-rule',
@ -56,24 +56,20 @@ import {
export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
@coerceBoolean()
disabled: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@coerceBoolean()
required: boolean;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
private modelValue: AlarmRule;
alarmRuleFormGroup= this.fb.group({
condition: [null, [Validators.required]],
alarmRuleFormGroup = this.fb.group({
condition: this.fb.control({}, Validators.required),
alarmDetails: [null],
dashboardId: [null]
});
@ -81,7 +77,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder,
private fb: FormBuilder,
private destroyRef: DestroyRef) {
}
@ -147,7 +143,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
private updateModel() {
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);
}
}

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

@ -20,5 +20,41 @@
display: grid;
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.
-->
<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()">
<form>
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'alarm-rule.schedule-title' : 'alarm-rule.edit-schedule') | translate }}</h2>
<span class="flex-1"></span>
<tb-toggle-select [(ngModel)]="settingsMode" [ngModelOptions]="{standalone: true}" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option value="static">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option>
<tb-toggle-option value="dynamic">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option>
<tb-toggle-select [formControl]="dynamicModeControl" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="false">{{ 'alarm-rule.static-schedule' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.dynamic-schedule' | translate }}</tb-toggle-option>
</tb-toggle-select>
<button mat-icon-button
(click)="cancel()"
@ -29,26 +29,25 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<tb-cf-alarm-schedule [arguments]="arguments"
[settingsMode]="settingsMode"
formControlName="alarmSchedule">
[dynamicMode]="dynamicModeControl.value"
[formControl]="alarmScheduleControl">
</tb-cf-alarm-schedule>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
type="button"
(click)="save()"
[disabled]="alarmScheduleControl.invalid || !alarmScheduleControl.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
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 { 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 { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@ -36,57 +33,38 @@ export interface AlarmRuleScheduleDialogData {
@Component({
selector: 'tb-cf-alarm-schedule-dialog',
templateUrl: './cf-alarm-schedule-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmScheduleDialogComponent}],
providers: [],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class CfAlarmScheduleDialogComponent extends DialogComponent<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>
implements OnInit, ErrorStateMatcher {
export class CfAlarmScheduleDialogComponent extends DialogComponent<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>{
readonly = this.data.readonly;
alarmSchedule = this.data.alarmSchedule;
arguments = this.data.arguments;
alarmScheduleFormGroup: UntypedFormGroup;
alarmScheduleControl = this.fb.control<AlarmRuleSchedule>(null);
submitted = false;
settingsMode: 'static' | 'dynamic' = 'static';
dynamicModeControl = this.fb.control(false);
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleScheduleDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>,
private fb: UntypedFormBuilder,
private utils: UtilsService,
public translate: TranslateService) {
private fb: FormBuilder) {
super(store, router, dialogRef);
this.alarmScheduleFormGroup = this.fb.group({
alarmSchedule: [this.alarmSchedule]
});
this.settingsMode = this.alarmSchedule?.dynamicValueArgument ? 'dynamic' : 'static';
this.alarmScheduleControl.patchValue(this.alarmSchedule, {emitEvent: false});
this.dynamicModeControl.patchValue(!!this.alarmSchedule?.dynamicValueArgument, {emitEvent: false});
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 {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.alarmSchedule = this.alarmScheduleFormGroup.get('alarmSchedule').value;
this.dialogRef.close(this.alarmSchedule);
this.dialogRef.close(this.alarmScheduleControl.value);
}
}

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">
@if (settingsMode === 'static') {
@if (!dynamicMode) {
<ng-container formGroupName="staticValue">
<mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="type" required placeholder="{{ 'alarm-rule.schedule-type' | translate }}">
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType">
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }}
</mat-option>
@for (scheduleType of alarmScheduleTypes; track scheduleType) {
<mat-option [value]="scheduleType">{{ alarmScheduleTypeTranslate.get(scheduleType) | translate }}</mat-option>
}
</mat-select>
<mat-error *ngIf="alarmScheduleForm.get('staticValue.type').hasError('required')">
{{ 'alarm-rule.schedule-type-required' | translate }}
</mat-error>
@if (alarmScheduleForm.get('staticValue.type').hasError('required')) {
<mat-error>{{ 'alarm-rule.schedule-type-required' | translate }}</mat-error>
}
</mat-form-field>
</ng-container>
@if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) {
@ -38,66 +38,67 @@
required
formControlName="timezone">
</tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME">
<mat-chip-listbox multiple formControlName="daysOfWeek">
<mat-chip-option *ngFor="let day of allDays; let i = index;"
[value]="i+1">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-option>
</mat-chip-listbox>
<tb-error class="block mb-2" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
<div class="flex flex-row xs:flex-col gt-xs:gap-2">
<div class="flex flex-row gap-2 gt-md:flex-1">
<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-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 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">
@if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) {
<section>
<mat-chip-listbox multiple formControlName="daysOfWeek">
<mat-chip-option *ngFor="let day of allDays; let i = index;"
[value]="i+1">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-option>
</mat-chip-listbox>
<tb-error class="block mb-2" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
<div class="flex flex-row xs:flex-col gt-xs:gap-2">
<div class="flex flex-row gap-2 gt-md:flex-1">
<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-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-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 sm:max-w-[120px] mb-[22px]"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
<div class="flex flex-1 items-center justify-center mb-[22px]">
<div class="text-center" [innerHTML]="getSchedulerRangeText(alarmScheduleForm.get('staticValue'))"></div>
</div>
</div>
</div>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section>
</section>
}
@if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) {
<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>
}
@ -110,11 +111,11 @@
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
<mat-error *ngIf="alarmScheduleForm.get('dynamicValueArgument').hasError('required')">
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
@if (alarmScheduleForm.get('dynamicValueArgument').hasError('required')) {
<mat-error>{{ 'calculated-fields.hint.argument-name-required' | translate }}</mat-error>
}
</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-placement="left"
[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.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
CustomTimeSchedulerItem,
dayOfWeekTranslations,
getAlarmScheduleRangeText,
timeOfDayToUTCTimestamp,
@ -43,6 +44,7 @@ import {
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { MatChipSelectionChange } from "@angular/material/chips";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({
selector: 'tb-cf-alarm-schedule',
@ -58,43 +60,38 @@ import { MatChipSelectionChange } from "@angular/material/chips";
multi: true
}]
})
export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit {
export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
private settingsModeValue: 'static' | 'dynamic';
get settingsMode(): 'static' | 'dynamic' {
return this.settingsModeValue;
}
@Input()
set settingsMode(value: 'static' | 'dynamic') {
if (value !== this.settingsModeValue && this.alarmScheduleForm) {
this.settingsModeValue = value;
this.updateModeValidators(value);
this.updateModel();
}
}
@coerceBoolean()
dynamicMode: boolean;
alarmScheduleForm = this.fb.group({
staticValue: this.fb.group({
type: [AlarmRuleScheduleType.ANY_TIME, Validators.required],
timezone: [null, Validators.required],
daysOfWeek: [null, Validators.required],
startsOn: [0, Validators.required],
endsOn: [0, Validators.required],
timezone: ['', Validators.required],
daysOfWeek: this.fb.control<number[] | null>(null, Validators.required),
startsOn: this.fb.control<Date | number>(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),
}),
dynamicValueArgument: [null, Validators.required]
dynamicValueArgument: ['', Validators.required]
});
alarmScheduleTypes = Object.keys(AlarmRuleScheduleType);
alarmScheduleTypes = Object.keys(AlarmRuleScheduleType) as Array<AlarmRuleScheduleType>;
alarmScheduleType = AlarmRuleScheduleType;
alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations;
argumentsList: Array<string>;
allDays = Array(7).fill(0).map((x, i) => i);
private modelValue: AlarmRuleSchedule;
@ -102,23 +99,24 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
private defaultItems = Array.from({length: 7}, (value, i) => ({
enabled: true,
dayOfWeek: i + 1
}));
})) as CustomTimeSchedulerItem[];
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder,
constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
this.alarmScheduleForm.get('staticValue.type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((type) => {
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.updateValidators(type);
this.alarmScheduleForm.updateValueAndValidity();
});
this.alarmScheduleForm.valueChanges.pipe(
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 {
const items: any[] = control.value;
if (!items || !items.length || !items.find(v => v.enabled === true)) {
@ -155,7 +163,7 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (this.disabled) {
this.alarmScheduleForm.disable({emitEvent: false});
} else {
this.updateModeValidators(this.settingsMode);
this.updateModeValidators(this.dynamicMode);
}
}
@ -163,10 +171,8 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
if (value) {
this.modelValue = value;
if (this.modelValue.dynamicValueArgument) {
this.settingsModeValue = 'dynamic';
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(this.modelValue.dynamicValueArgument, {emitEvent: false});
} else {
this.settingsModeValue = 'static';
switch (this.modelValue.staticValue.type) {
case AlarmRuleScheduleType.SPECIFIC_TIME:
this.alarmScheduleForm.patchValue({
@ -212,11 +218,11 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
}
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 : {
alarmScheduler: {
valid: false
@ -224,14 +230,14 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
};
}
private updateModeValidators(mode: 'static' | 'dynamic') {
if (mode === 'static') {
private updateModeValidators(mode: boolean) {
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('dynamicValueArgument').disable({emitEvent: false});
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.startsOn').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;
}
}
private updateModel() {
const value = this.alarmScheduleForm.value as AlarmRuleSchedule;
if (this.settingsMode === 'static') {
if (!this.dynamicMode) {
if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) {
value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn);
}
@ -271,22 +277,18 @@ export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator
value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn);
}
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) => {
return { ...item, startsOn: timeOfDayToUTCTimestamp(item.startsOn), endsOn: timeOfDayToUTCTimestamp(item.endsOn)};
});
}
}
this.modelValue = value;
if (this.alarmScheduleForm.valid) {
this.propagateChange(this.modelValue);
} else {
this.propagateChange(null);
}
this.propagateChange(this.modelValue);
}
private defaultItemsScheduler(index): UntypedFormGroup {
private defaultItemsScheduler(index: number): FormGroup {
return this.fb.group({
enabled: [true],
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);
}
get itemsSchedulerForm(): UntypedFormArray {
return this.alarmScheduleForm.get('staticValue.items') as UntypedFormArray;
get itemsSchedulerForm(): FormArray {
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.
-->
<!--
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">
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let index = $index) {
<div class="flex flex-row items-center justify-start gap-2 pb-2" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule flex flex-1 flex-col gap-4" [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="min-w-40 xs:min-w-fit" translate>alarm.severity</div>
<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.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, DestroyRef, forwardRef, Input } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator,
Validators
} from '@angular/forms';
import { Subject } from 'rxjs';
import { AlarmSeverity, alarmSeverityTranslations } from '@shared/models/alarm.models';
import { takeUntil } from 'rxjs/operators';
import { AlarmRule } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({
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 {
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors;
export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Validator {
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
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 destroy$ = new Subject<void>();
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 {
@ -82,20 +89,6 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
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 {
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> = [];
if (createAlarmRules) {
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) {
this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else {
@ -136,11 +131,11 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
}
public removeCreateAlarmRule(index: number) {
(this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray).removeAt(index);
(this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray).removeAt(index);
}
public addCreateAlarmRule() {
const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray;
const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as FormArray;
createAlarmRulesArray.push(this.fb.group({
severity: [this.getFirstUnusedSeverity(), Validators.required],
alarmRule: [null, Validators.required]
@ -176,15 +171,15 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit
private updateUsedSeverities() {
this.usedSeverities = [];
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
value.forEach((rule, index) => {
this.usedSeverities[index] = AlarmSeverity[rule.severity];
});
}
private updateModel() {
const value: {severity: string, alarmRule: AlarmRule}[] = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules: {[severity: string]: AlarmRule} = {};
const value = this.createAlarmRulesFormGroup.get('createAlarmRules').value;
const createAlarmRules = {} as Record<AlarmSeverity, AlarmRule>;
value.forEach(v => createAlarmRules[v.severity] = v.alarmRule);
this.updateUsedSeverities();
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;">
<mat-toolbar color="primary">
<mat-toolbar class="justify-between" color="primary">
<h2 translate>filter.complex-filter</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -26,33 +25,30 @@
</button>
</mat-toolbar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col">
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</fieldset>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<tb-alarm-rule-filter-predicate-list [valueType]="data.valueType"
[arguments]="arguments"
[operation]="complexFilterFormGroup.get('operation').value"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
[disabled]="complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
Validators
} from '@angular/forms';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
@ -35,8 +28,7 @@ import {
EntityKeyValueType,
FilterPredicateType
} from '@shared/models/query/query.models';
import { ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export interface AlarmRuleComplexFilterPredicateDialogData {
@ -54,10 +46,14 @@ export interface AlarmRuleComplexFilterPredicateDialogData {
})
export class AlarmRuleComplexFilterPredicateDialogComponent extends
DialogComponent<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate>
implements OnInit, ErrorStateMatcher {
DialogComponent<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate> {
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);
complexOperationEnum = ComplexOperation;
@ -65,35 +61,18 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
isAdd: boolean;
submitted = false;
arguments = this.data.arguments;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleComplexFilterPredicateDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate>,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
super(store, router, dialogRef);
this.isAdd = this.data.isAdd;
this.complexFilterFormGroup = this.fb.group(
{
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;
this.complexFilterFormGroup.patchValue(this.data.complexPredicate, {emitEvent: false});
}
cancel(): void {
@ -101,7 +80,6 @@ export class AlarmRuleComplexFilterPredicateDialogComponent extends
}
save(): void {
this.submitted = true;
if (this.complexFilterFormGroup.valid) {
const predicate: ComplexAlarmRuleFilterPredicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
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;">
<mat-toolbar color="primary">
<h2>{{(data.isAdd ? 'alarm-rule.add-filter' : ('alarm-rule.edit-filter')) | translate}}</h2>
<span class="flex-1"></span>
<mat-toolbar class="justify-between" color="primary">
<h2>{{(data.isAdd ? 'alarm-rule.add-filter' : 'alarm-rule.edit-filter') | translate}}</h2>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -26,7 +25,7 @@
</button>
</mat-toolbar>
<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">
<div class="tb-form-panel-title">{{ 'alarm-rule.general' | translate }}</div>
<div class="flex flex-row gap-2 xs:flex-col">
@ -81,20 +80,18 @@
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>
</fieldset>
</section>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterFormGroup.invalid || !filterFormGroup.dirty">
[disabled]="filterFormGroup.invalid || !filterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</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.
///
import { Component, Inject, OnDestroy, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { Component, DestroyRef, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormGroup,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
Validators
} from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
@ -37,11 +29,10 @@
} from '@shared/models/query/query.models';
import { DialogService } from '@core/services/dialog.service';
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 { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter;
@ -53,13 +44,10 @@
@Component({
selector: 'tb-alarm-rule-filter-dialog',
templateUrl: './alarm-rule-filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleFilterDialogComponent}],
providers: [],
styleUrls: ['./alarm-rule-filter-dialog.component.scss']
})
export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFilterDialogComponent, AlarmRuleFilter>
implements OnDestroy, ErrorStateMatcher {
private destroy$ = new Subject<void>();
export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFilterDialogComponent, AlarmRuleFilter> {
filterFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilter>>;
@ -73,22 +61,21 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
ComplexOperation = ComplexOperation;
submitted = false;
searchText = '';
arguments = this.data.arguments;
argumentsList: Array<string>;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleFilterDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<AlarmRuleFilterDialogComponent, AlarmRuleFilter>,
private dialogs: DialogService,
private translate: TranslateService,
private fb: UntypedFormBuilder) {
private fb: FormBuilder,
private destroyRef: DestroyRef) {
super(store, router, dialogRef);
this.argumentsList = this.arguments ? Object.keys(this.arguments) : [];
this.filterFormGroup = this.fb.group(
{
argument: [this.data.filter.argument, [Validators.required]],
@ -98,7 +85,7 @@ export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFil
}
);
this.filterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$)
takeUntilDestroyed(this.destroyRef)
).subscribe((valueType: EntityKeyValueType) => {
const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType;
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 {
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 {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
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) : [];
this.dialogRef.close(this.filterFormGroup.value as AlarmRuleFilter);
}
}

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.
-->
<section class="tb-form-panel no-padding no-gap stroked" [formGroup]="filterListFormGroup">
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span style="min-width: 96px;">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="filter-list">
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start" style="max-height: 76px;"
formArrayName="filters"
[class.filter-list-divider]="index">
<div class="filters-operation max-w-8% flex-full">
@if ($index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</div>
}
</div>
<div class="flex max-w-92% flex-full flex-col">
<div class="flex flex-row items-center justify-start">
<div class="flex-1">{{ 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>
<section class="tb-form-panel no-padding no-gap stroked mb-4" [formGroup]="filterListFormGroup">
<div class="flex flex-row">
<span class="max-w-8% flex-full"></span>
<div class="flex max-w-92% flex-full flex-row items-center justify-start">
<label translate class="filter-title flex-1">alarm-rule.argument-name</label>
<label translate class="filter-title flex-1">alarm-rule.value-type</label>
<span class="min-w-24">&nbsp;</span>
</div>
</div>
<mat-divider></mat-divider>
<div class="filter-list">
@for (filterControl of filtersFormArray.controls; track filterControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start"
formArrayName="filters"
[class.filter-list-divider]="index">
<div class="filters-operation max-w-8% flex-full">
@if ($index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslationMap.get(operation) | translate }}</span>
</div>
@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>
@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>
}
<span [class.!hidden]="!!filtersFormArray.length"
class="no-data-found flex items-center justify-center"
translate>
filter.no-key-filters
</span>
</div>
</section>
<div style="margin-top: 16px;">
<button mat-button mat-raised-button color="primary"
(click)="addFilter()"
type="button"
matTooltip="{{ 'alarm-rule.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'alarm-rule.add-filter' | translate }}
</button>
</div>
<button mat-button mat-raised-button color="primary"
(click)="addFilter()"
type="button"
matTooltip="{{ 'alarm-rule.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'alarm-rule.add-filter' | translate }}
</button>

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

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.
-->
<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="flex flex-row">
<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-50% flex-full">alarm-rule.value</label>
</div>
<span [class.!hidden]="disabled" style="min-width: 40px;">&nbsp;</span>
<span [class.!hidden]="disabled" class="min-w-10">&nbsp;</span>
</div>
</div>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
@for (predicateControl of predicatesFormArray.controls; track predicateControl; let index = $index) {
<div class="flex flex-row items-stretch justify-start relative"
[class.key-filter-list-divider]="$index"
formArrayName="predicates">
[class.key-filter-list-divider]="$index">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
@ -60,25 +59,27 @@
}
<span [class.!hidden]="!!predicatesFormArray.length"
[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 style="margin-top: 16px;" class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-complex' | translate }}
</button>
</div>
<div class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
{{ 'filter.add-complex' | translate }}
</button>
</div>
</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.
///
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { Component, DestroyRef, forwardRef, Input } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import { Observable, of } from 'rxjs';
import {
BooleanOperation,
ComplexOperation,
@ -39,9 +38,7 @@ import {
StringOperation
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog';
import { map, takeUntil } from 'rxjs/operators';
import { ComponentType } from '@angular/cdk/portal';
import { COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN } from '@home/components/tokens';
import { map } from 'rxjs/operators';
import {
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleComplexFilterPredicateDialogData
@ -52,6 +49,7 @@ import {
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
@Component({
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;
@ -80,36 +78,26 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
@Input() arguments: Record<string, CalculatedFieldArgument>;
filterListFormGroup: UntypedFormGroup;
filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
valueTypeEnum = EntityKeyValueType;
complexOperationTranslations = complexOperationTranslationMap;
private destroy$ = new Subject<void>();
private propagateChange = null;
private propagateChange= (v: any) => { };
constructor(private fb: UntypedFormBuilder,
@Inject(COMPLEX_FILTER_PREDICATE_DIALOG_COMPONENT_TOKEN) private complexFilterPredicateDialogComponent: ComponentType<any>,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.filterListFormGroup = this.fb.group({
predicates: this.fb.array([])
});
constructor(private fb: FormBuilder,
private dialog: MatDialog,
private destroyRef: DestroyRef) {
this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
takeUntilDestroyed(this.destroyRef)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get predicatesFormArray(): UntypedFormArray {
return this.filterListFormGroup.get('predicates') as UntypedFormArray;
get predicatesFormArray(): FormArray {
return this.filterListFormGroup.get('predicates') as FormArray;
}
registerOnChange(fn: any): void {
@ -135,22 +123,14 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
}
writeValue(predicates: Array<AlarmRulePredicateInfo>): void {
if (predicates?.length === this.predicatesFormArray.length) {
this.predicatesFormArray.patchValue(predicates, {emitEvent: false});
} else {
const predicateControls: Array<AbstractControl> = [];
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});
const predicateControls: Array<AbstractControl> = [];
if (predicates) {
for (const predicate of predicates) {
predicateControls.push(this.fb.control(predicate, [Validators.required]));
}
}
this.predicatesFormArray.clear();
predicateControls.forEach(predicate => this.predicatesFormArray.push(predicate));
}
public removePredicate(index: number) {
@ -158,7 +138,7 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
}
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);
let observable: Observable<AlarmRuleFilterPredicate>;
if (complex) {
@ -217,23 +197,11 @@ export class AlarmRuleFilterPredicateListComponent implements ControlValueAccess
arguments: this.arguments,
}
}).afterClosed().pipe(
map((result) => {
if (result) {
predicate = result;
return predicate;
} else {
return null;
}
})
map(result => result)
);
}
private updateModel() {
const predicates: Array<AlarmRulePredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
if (predicates.length) {
this.propagateChange(predicates);
} else {
this.propagateChange(null);
}
this.propagateChange(this.filterListFormGroup.get('predicates').value);
}
}

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-row no-padding no-border column-xs">
<mat-form-field hideRequiredMarker class="flex-1 w-full max-w-30% xs:max-w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select [ngModel]="mode"
(ngModelChange)="onModeChange($event)"
[ngModelOptions]="{ standalone: true }"
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 [formControl]="dynamicModeControl" placeholder="{{'filter.dynamic-source-type' | translate}}">
<mat-option [value]="false">{{'alarm-rule.static' | translate}}</mat-option>
<mat-option [value]="true">{{'alarm-rule.dynamic' | translate}}</mat-option>
</mat-select>
</mat-form-field>
@if (mode === 'static') {
@if (!dynamicModeControl.value) {
@switch (valueType) {
@case (valueTypeEnum.STRING) {
<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 {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
ValidationErrors,
Validator,
ValidatorFn,
@ -30,7 +30,6 @@ import { EntityKeyValueType } from '@shared/models/query/query.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { AlarmRuleValue } from "@shared/models/alarm-rule.models";
import { isDefinedAndNotNull } from "@core/utils";
import { FormControlsFrom } from "@shared/models/tenant.model";
@Component({
@ -62,16 +61,18 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
filterPredicateValueFormGroup: FormGroup<FormControlsFrom<AlarmRuleValue<string | number | boolean>>>;
mode: 'static' | 'dynamic' = 'static';
dynamicModeControl = this.fb.control(false);
private propagateChange = null;
private propagateChangePending = false;
argumentsList: Array<string>;
constructor(private fb: UntypedFormBuilder,
private propagateChange= (v: any) => { };
constructor(private fb: FormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
this.argumentsList = this.arguments ? Object.keys(this.arguments): [];
let defaultValue: string | number | boolean;
let defaultValueValidators: ValidatorFn[];
switch (this.valueType) {
@ -94,42 +95,30 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
this.filterPredicateValueFormGroup = this.fb.group({
staticValue: [defaultValue, defaultValueValidators],
dynamicValueArgument: [null, Validators.required]
dynamicValueArgument: ['', Validators.required]
});
this.filterPredicateValueFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
this.dynamicModeControl.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(value => this.updateValueModeValidators(value))
}
private updateValueModeValidators(mode: 'static' | 'dynamic'): void {
if (mode === 'static') {
this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
} else {
private updateValueModeValidators(isDynamicMode: boolean): void {
if (isDynamicMode) {
this.filterPredicateValueFormGroup.get('staticValue').disable({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 {
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 {
@ -142,18 +131,11 @@ export class AlarmRuleFilterPredicateValueComponent implements ControlValueAcces
}
writeValue(predicateValue: AlarmRuleValue<string | number | boolean>): void {
this.propagateChangePending = false;
this.filterPredicateValueFormGroup.patchValue(predicateValue, {emitEvent: false});
this.mode = isDefinedAndNotNull(predicateValue.dynamicValueArgument) ? 'dynamic' : 'static';
this.updateValueModeValidators(this.mode);
this.dynamicModeControl.patchValue(!!predicateValue.dynamicValueArgument?.length);
}
private updateModel() {
const predicateValue: AlarmRuleValue<string | number | boolean> = this.filterPredicateValueFormGroup.value;
if (this.propagateChange) {
this.propagateChange(predicateValue);
} else {
this.propagateChangePending = true;
}
this.propagateChange(this.filterPredicateValueFormGroup.value);
}
}

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

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"
tbTruncateWithTooltip
[class.required]="requiredClass"
[class.required]="isRequired"
[class.nowrap]="nowrap"
[innerHTML]="filterText">
</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;
@ -95,7 +95,7 @@ export class AlarmRuleFilterTextComponent {
}
private updateFilterText(value: AlarmRuleExpression) {
this.requiredClass = false;
this.isRequired = false;
if (value && (value.expression || value.filters)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
@ -108,7 +108,7 @@ export class AlarmRuleFilterTextComponent {
} else {
if (this.required) {
this.filterText = this.addFilterPrompt;
this.requiredClass = true;
this.isRequired = true;
} else {
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,
ArgumentType,
ArgumentTypeTranslations,
CalculatedFieldArgumentValue, CFArgumentDynamicSourceType,
CalculatedFieldArgumentValue,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models';
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.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId);
this.updateEntityFilter(this.entityType, true);
this.updatedRefEntityIdState(this.entityType);
this.updatedRefEntityIdState(this.entityType, false);
this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType();
this.setInitialEntityType();
@ -168,7 +168,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
saveArgument(): void {
const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue;
if (this.entityType === ArgumentEntityType.Owner) {
value.refDynamicSource = CFArgumentDynamicSourceType.CURRENT_OWNER;
value.refDynamicSourceConfiguration.type = ArgumentEntityType.Owner;
} else if (this.entityType === ArgumentEntityType.Tenant) {
value.refEntityId = new TenantId(this.tenantId) as any;
}
@ -188,7 +188,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
private updatedArgumentType(): void {
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;
}
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;
this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']();
this.argumentFormGroup.get('refEntityId')[isEntityWithId ? 'enable' : 'disable']({emitEvent});
if (!isEntityWithId) {
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>
@if (argument.refEntityId?.entityType === ArgumentEntityType.Tenant) {
{{ 'calculated-fields.argument-current-tenant' | translate }}
} @else if (argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) {
{{ 'calculated-fields.argument-owner' | translate }}
} @else if (argument.refEntityId?.id) {
{{ entityTypeTranslations.get(argument.refEntityId.entityType).type | translate }}
} @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">
<mat-toolbar color="primary">
<h2>{{ 'calculated-fields.debugging' | translate}}</h2>
<h2>{{ dialogTitle | translate}}</h2>
<span class="flex-1"></span>
<button mat-icon-button
(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 EventType = EventType;
dialogTitle: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDebugDialogData,
protected dialogRef: MatDialogRef<CalculatedFieldDebugDialogComponent, string>) {
super(store, router, dialogRef);
this.dialogTitle = this.data.value.type === CalculatedFieldType.ALARM ? 'alarm-rule.debugging' : 'calculated-fields.debugging';
}
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 }) allEnabled = false;
@Input({ transform: booleanAttribute }) isAlarmRule = false;
@Input() entityType: EntityType;
@Input() allEnabledUntil = 0;
@Input() maxDebugModeDuration = getCurrentAuthState(this.store).maxDebugModeDurationMinutes * MINUTE;
@ -109,7 +110,11 @@ export class EntityDebugSettingsPanelComponent extends PageComponent implements
this.onFailuresControl.patchValue(this.failuresEnabled);
this.debugAllControl.patchValue(this.allEnabled);
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 {

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

@ -28,6 +28,7 @@ export interface EntityDebugSettingPanelConfig {
maxDebugModeDuration?: number;
additionalActionConfig?: AdditionalDebugActionConfig;
entityType: EntityType;
isAlarmRule?: boolean;
}
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>
</ng-template>
</mat-expansion-panel>
<mat-expansion-panel [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title>
<div>{{'device-profile.alarm-rules-with-count' | translate:
{count: entityForm.get('profileData.alarms').value ?
entityForm.get('profileData.alarms').value.length : 0} }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="deviceProfileId">
</tb-device-profile-alarms>
</ng-template>
</mat-expansion-panel>
@if (entityForm.get('profileData.alarms').value?.length) {
<mat-expansion-panel [expanded]="false">
<mat-expansion-panel-header>
<mat-panel-title>
<div>{{'device-profile.alarm-rules-with-count' | translate:
{count: entityForm.get('profileData.alarms').value ?
entityForm.get('profileData.alarms').value.length : 0} }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="deviceProfileId">
</tb-device-profile-alarms>
</ng-template>
</mat-expansion-panel>
}
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<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">
@if (hasOldRules && !isEdit) {
<div class="flex flex-row items-center justify-end" style="padding: 16px 16px 0;">
<tb-toggle-select [(ngModel)]="alarmRulesVersion" 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-old' | translate }}</tb-toggle-option>
<tb-toggle-select [(ngModel)]="alarmRulesOldVersion" appearance="fill">
<tb-toggle-option [value]="false">{{ 'alarm-rule.alarm-rules-new' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="true">{{ 'alarm-rule.alarm-rules-old' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
}
@if (alarmRulesVersion) {
<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 {
@if (alarmRulesOldVersion) {
<div class="mat-padding" [formGroup]="detailsForm" *ngIf="alarmRules.isActive">
<div formGroupName="profileData">
<tb-device-profile-alarms formControlName="alarms" [deviceProfileId]="entity.id"></tb-device-profile-alarms>
</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>
</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;
hasOldRules = false;
alarmRulesVersion = false;
alarmRulesOldVersion = false;
constructor(protected store: Store<AppState>,
private destroyRef: DestroyRef) {
@ -61,7 +61,7 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
protected setEntity(entity: DeviceProfile) {
this.isTransportTypeChanged = false;
this.hasOldRules = !!entity?.profileData?.alarms?.length;
this.alarmRulesVersion = !this.hasOldRules;
this.alarmRulesOldVersion = this.isEdit || false;
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 { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
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> {
entityId: EntityId;
@ -139,11 +140,11 @@ interface BasePropagationConfiguration {
interface CalculatedFieldAlarmRuleConfiguration {
type: CalculatedFieldType.ALARM;
arguments: Record<string, CalculatedFieldArgument>;
createRules: {[severity: string]: AlarmRule};
createRules: Record<AlarmSeverity, AlarmRule>;
clearRule?: AlarmRule;
propagate?: boolean;
propagateToOwner?: boolean;
propagateToTenant?: boolean;
propagate: boolean;
propagateToOwner: boolean;
propagateToTenant: boolean;
propagateRelationTypes?: Array<string>;
}
@ -266,15 +267,11 @@ export const ArgumentTypeTranslations = new Map<ArgumentType, string>(
]
)
export enum CFArgumentDynamicSourceType {
CURRENT_OWNER = 'CURRENT_OWNER'
}
export interface CalculatedFieldArgument {
refEntityKey: RefEntityKey;
defaultValue?: string;
refEntityId?: RefEntityId;
refDynamicSource?: CFArgumentDynamicSourceType;
refDynamicSourceConfiguration?: RefDynamicSourceConfiguration;
limit?: number;
timeWindow?: number;
}
@ -338,7 +335,7 @@ export interface CalculatedFieldGeofencing {
}
export interface RefDynamicSourceConfiguration {
type?: ArgumentEntityType.RelationQuery;
type?: ArgumentEntityType.RelationQuery | ArgumentEntityType.Owner;
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-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-tenant": "Propagate alarm to Tenant"
"propagate-alarm-to-tenant": "Propagate alarm to Tenant",
"debugging": "Alarm rule debugging"
},
"ai-models": {
"ai-models": "AI models",

Loading…
Cancel
Save