Browse Source

UI: Calculated field Alarm Rules

pull/14276/head
ArtemDzhereleiko 10 months ago
parent
commit
0102a84f94
  1. 15
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  2. 54
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.html
  3. 82
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-details-dialog.component.ts
  4. 164
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  5. 70
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.scss
  6. 183
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  7. 94
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  8. 278
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  9. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
  10. 22
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
  11. 88
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  12. 196
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  13. 244
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  14. 52
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  15. 37
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.scss
  16. 283
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  17. 50
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  18. 35
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
  19. 153
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  20. 24
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
  21. 54
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.html
  22. 92
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule-dialog.component.ts
  23. 124
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
  24. 325
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.ts
  25. 80
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  26. 36
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.scss
  27. 192
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  28. 59
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.html
  29. 111
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component.ts
  30. 101
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  31. 28
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.scss
  32. 153
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.ts
  33. 81
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  34. 50
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.scss
  35. 195
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.ts
  36. 84
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  37. 50
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.scss
  38. 239
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.ts
  39. 80
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.html
  40. 159
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component.ts
  41. 79
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  42. 158
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.ts
  43. 23
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.html
  44. 85
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.scss
  45. 192
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-text.component.ts
  46. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  47. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts
  48. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  49. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  50. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  51. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  52. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html
  53. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  54. 5
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  55. 40
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html
  56. 93
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  57. 94
      ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html
  58. 136
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  59. 5
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  60. 89
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  61. 152
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  62. 33
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  63. 123
      ui-ngx/src/assets/help/en_US/alarm-rule/alarm_rule_schedule_format.md
  64. 132
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -19,7 +19,11 @@ import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/models/calculated-field.models';
import {
CalculatedField,
CalculatedFieldTestScriptInputParams,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
@ -46,9 +50,12 @@ export class CalculatedFieldsService {
return this.http.delete<boolean>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
}
public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
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));
}
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {

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

@ -0,0 +1,54 @@
<!--
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.
-->
<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">
<h2>{{ 'alarm-rule.edit-alarm-rule-additional-info' | translate }}</h2>
<span class="flex-1"></span>
<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>
<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 *ngIf="!data.readonly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,82 @@
///
/// 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.
///
import { Component, Inject, OnInit, SkipSelf } 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 { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } 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;
readonly: boolean;
}
@Component({
selector: 'tb-edit-alarm-details-dialog',
templateUrl: './alarm-rule-details-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleDetailsDialogComponent}],
styleUrls: []
})
export class AlarmRuleDetailsDialogComponent extends DialogComponent<AlarmRuleDetailsDialogComponent, string>
implements OnInit, ErrorStateMatcher {
alarmDetails = this.data.alarmDetails;
editDetailsFormGroup: UntypedFormGroup;
submitted = false;
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) {
super(store, router, dialogRef);
this.editDetailsFormGroup = this.fb.group({
alarmDetails: [this.alarmDetails]
});
if (this.data.readonly) {
this.editDetailsFormGroup.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);
}
}

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

@ -0,0 +1,164 @@
<!--
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 [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex size-full max-w-4xl flex-col">
<mat-toolbar color="primary">
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2>
<span class="flex-1"></span>
<div tb-help="alarmRule"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content class="flex-1">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'common.general' | translate }}</div>
<div class="flex items-center gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'alarm-rule.alarm-type' | translate }}</mat-label>
<input matInput maxlength="255" formControlName="name" required>
@if (fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched) {
<mat-error>
@if (fieldFormGroup.get('name').hasError('required')) {
{{ 'alarm-rule.alarm-type-required' | translate }}
} @else if (fieldFormGroup.get('name').hasError('pattern')) {
{{ 'alarm-rule.alarm-type-pattern' | translate }}
} @else if (fieldFormGroup.get('name').hasError('maxlength')) {
{{ 'alarm-rule.alarm-type-max-length' | translate }}
}
</mat-error>
}
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
</div>
<ng-container [formGroup]="configFormGroup">
<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"
[entityId]="data.entityId"
[tenantId]="data.tenantId"
[ownerId]="data.ownerId"
[entityName]="data.entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-alarm-rules' | translate }}</div>
<div class="flex flex-1 flex-col">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments">
</tb-create-cf-alarm-rules>
</div>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.clear-alarm-rule' | translate }}</div>
<div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value">
<div class="clear-alarm-rule flex flex-1 flex-row">
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments">
</tb-cf-alarm-rule>
</div>
<button mat-icon-button
class="button-icon"
type="button"
(click)="removeClearAlarmRule()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate style="margin: 16px 0"
class="tb-prompt flex items-center justify-center">alarm-rule.no-clear-alarm-rule</span>
</div>
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'alarm-rule.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
</button>
</div>
</div>
<div class="tb-form-panel no-gap">
<mat-expansion-panel class="tb-settings" [expanded]="false">
<mat-expansion-panel-header>
{{ 'alarm-rule.advanced-settings' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagate">
{{ 'alarm-rule.propagate-alarm' | translate }}
</mat-slide-toggle>
</div>
@if (configFormGroup.get('propagate').value) {
<mat-form-field floatLabel="always" class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList>
<mat-chip-row
*ngFor="let key of configFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'alarm-rule.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'alarm-rule.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
}
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToOwner">
{{ 'alarm-rule.propagate-alarm-to-owner' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide margin" formControlName="propagateToTenant">
{{ 'alarm-rule.propagate-alarm-to-tenant' | translate }}
</mat-slide-toggle>
</div>
</ng-template>
</mat-expansion-panel>
</div>
</ng-container>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || fieldFormGroup.invalid || !fieldFormGroup.dirty">
{{ data.buttonTitle | translate }}
</button>
</div>
</div>

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

@ -0,0 +1,70 @@
/**
* 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.
*/
.calculated-field-dialog-container {
width: 869px;
max-width: 100%;
}
.clear-alarm-rule {
border: 1px solid rgba(0, 0, 0, 0.12);
border-left-width: 4px;
border-left-color: green;
border-radius: 4px;
padding: 8px;
min-width: 0;
}
.button-icon {
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;
}
}
}
}

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

@ -0,0 +1,183 @@
///
/// 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.
///
import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import { CalculatedField, CalculatedFieldArgument, CalculatedFieldType } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { EntityType } from '@shared/models/entity-type.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips";
import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils";
export interface AlarmRuleDialogData {
value?: CalculatedField;
buttonTitle: string;
entityId: EntityId;
tenantId: string;
entityName?: string;
ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
isDirty?: boolean;
}
@Component({
selector: 'tb-alarm-rule-dialog',
templateUrl: './alarm-rule-dialog.component.html',
styleUrls: ['./alarm-rule-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedField> {
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({}),
propagate: [false],
propagateToOwner: [false],
propagateToTenant: [false],
propagateRelationTypes: [null],
createRules: [null],
clearRule: [null],
}),
});
additionalDebugActionConfig = this.data.value?.id ? {
...this.data.additionalDebugActionConfig,
action: () => this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }),
} : null;
readonly EntityType = EntityType;
readonly CalculatedFieldType = CalculatedFieldType;
readonly ScriptLanguage = ScriptLanguage;
separatorKeysCodes = [ENTER, COMMA, SEMICOLON];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedField>,
private calculatedFieldsService: CalculatedFieldsService,
private destroyRef: DestroyRef,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.observeIsLoading();
this.applyDialogData();
}
get configFormGroup(): FormGroup {
return this.fieldFormGroup.get('configuration') as FormGroup;
}
get arguments(): Record<string, CalculatedFieldArgument> {
return this.fieldFormGroup.get('configuration.arguments').value;
}
public removeClearAlarmRule() {
this.configFormGroup.patchValue({clearRule: null});
this.fieldFormGroup.markAsDirty();
}
public addClearAlarmRule() {
const clearAlarmRule: AlarmRule = {
condition: {
type: AlarmRuleConditionType.SIMPLE,
expression: {
type: AlarmRuleExpressionType.SIMPLE
}
}
};
this.configFormGroup.patchValue({clearRule: clearAlarmRule});
}
removeRelationType(key: string): void {
const keys: string[] = this.configFormGroup.get('propagateRelationTypes').value;
const index = keys.indexOf(key);
if (index >= 0) {
keys.splice(index, 1);
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
}
}
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 = [];
}
keys.push(value);
this.configFormGroup.get('propagateRelationTypes').setValue(keys, {emitEvent: true});
}
}
if (input) {
input.value = '';
}
}
get fromGroupValue(): CalculatedField {
return deepTrim(this.fieldFormGroup.value as CalculatedField);
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
if (this.fieldFormGroup.valid) {
const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue};
alarmRule.configuration.type = CalculatedFieldType.ALARM;
this.calculatedFieldsService.saveCalculatedField(alarmRule)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(calculatedField => this.dialogRef.close(calculatedField));
}
}
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});
}
private observeIsLoading(): void {
this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => {
if (loading) {
this.fieldFormGroup.disable({emitEvent: false});
} else {
this.fieldFormGroup.enable({emitEvent: false});
if (this.data.isDirty) {
this.fieldFormGroup.markAsDirty();
}
}
});
}
}

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

@ -0,0 +1,94 @@
///
/// 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.
///
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { AlarmRuleDialogComponent } from "@home/components/alarm-rules/alarm-rule-dialog.component";
import { CreateCfAlarmRulesComponent } from "@home/components/alarm-rules/create-cf-alarm-rules.component";
import { CfAlarmRuleComponent } from "@home/components/alarm-rules/cf-alarm-rule.component";
import { CfAlarmRuleConditionComponent } from "@home/components/alarm-rules/cf-alarm-rule-condition.component";
import {
CfAlarmRuleConditionDialogComponent
} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component";
import { CfAlarmScheduleComponent } from "@home/components/alarm-rules/cf-alarm-schedule.component";
import { CfAlarmScheduleDialogComponent } from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import {
EntityDebugSettingsButtonComponent
} from "@home/components/entity/debug/entity-debug-settings-button.component";
import { AlarmRuleFilterTextComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-text.component";
import {
CalculatedFieldArgumentsTableModule
} from "@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.module";
import {
AlarmRuleFilterPredicateListComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component";
import {
AlarmRuleFilterPredicateComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate.component";
import {
AlarmRuleFilterPredicateValueComponent
} from "@home/components/alarm-rules/filter/alarm-rule-filter-predicate-value.component";
import {
AlarmRuleComplexFilterPredicateDialogComponent
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import { AlarmRuleFilterListComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-list.component";
import { AlarmRuleFilterDialogComponent } from "@home/components/alarm-rules/filter/alarm-rule-filter-dialog.component";
import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
@NgModule({
declarations: [
AlarmRuleDialogComponent,
CreateCfAlarmRulesComponent,
CfAlarmRuleComponent,
CfAlarmRuleConditionComponent,
CfAlarmRuleConditionDialogComponent,
CfAlarmScheduleComponent,
CfAlarmScheduleDialogComponent,
AlarmRuleFilterTextComponent,
AlarmRuleFilterListComponent,
AlarmRuleFilterDialogComponent,
AlarmRuleFilterPredicateListComponent,
AlarmRuleFilterPredicateComponent,
AlarmRuleFilterPredicateValueComponent,
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
],
imports: [
CommonModule,
SharedModule,
EntityDebugSettingsButtonComponent,
CalculatedFieldArgumentsTableModule
],
exports: [
AlarmRuleDialogComponent,
CreateCfAlarmRulesComponent,
CfAlarmRuleComponent,
CfAlarmRuleConditionComponent,
CfAlarmRuleConditionDialogComponent,
CfAlarmScheduleComponent,
CfAlarmScheduleDialogComponent,
AlarmRuleFilterTextComponent,
AlarmRuleFilterListComponent,
AlarmRuleFilterDialogComponent,
AlarmRuleFilterPredicateListComponent,
AlarmRuleFilterPredicateComponent,
AlarmRuleFilterPredicateValueComponent,
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
]
})
export class AlarmRuleModule { }

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

@ -0,0 +1,278 @@
///
/// 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.
///
import {
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { EntityType } from '@shared/models/entity-type.models';
import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import {
ArgumentEntityType,
CalculatedField,
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';
import {
AlarmRuleDialogComponent,
AlarmRuleDialogData
} from "@home/components/alarm-rules/alarm-rule-dialog.component";
import {
CalculatedFieldDebugDialogComponent,
CalculatedFieldDebugDialogData
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
export class AlarmRulesTableConfig extends EntityTableConfig<any> {
readonly tenantId = getCurrentAuthUser(this.store).tenantId;
additionalDebugActionConfig = {
title: this.translate.instant('calculated-fields.see-debug-events'),
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
};
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
private datePipe: DatePipe,
public entityId: EntityId = null,
private store: Store<AppState>,
private destroyRef: DestroyRef,
private renderer: Renderer2,
public entityName: string,
private ownerId: EntityId = null,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
) {
super();
this.tableTitle = this.translate.instant('alarm-rule.alarm-rules');
this.detailsPanelEnabled = false;
this.pageMode = false;
this.entityType = EntityType.CALCULATED_FIELD;
this.entityTranslations = {
type: 'alarm-rule.alarm-rule',
typePlural: 'alarm-rule.alarm-rules',
list: 'alarm-rule.list',
add: 'action.add',
noEntities: 'alarm-rule.no-found',
search: 'action.search',
selectedEntities: 'alarm-rule.selected-fields'
};
this.entitiesFetchFunction = (pageLink: PageLink) => this.fetchCalculatedFields(pageLink);
this.addEntity = this.getCalculatedAlarmDialog.bind(this);
this.deleteEntityTitle = (field: CalculatedField) => this.translate.instant('alarm-rule.delete-title', {title: field.name});
this.deleteEntityContent = () => this.translate.instant('alarm-rule.delete-text');
this.deleteEntitiesTitle = count => this.translate.instant('alarm-rule.delete-multiple-title', {count});
this.deleteEntitiesContent = () => this.translate.instant('alarm-rule.delete-multiple-text');
this.deleteEntity = id => this.calculatedFieldsService.deleteCalculatedField(id.id);
this.addActionDescriptors = [
{
name: this.translate.instant('alarm-rule.create'),
icon: 'insert_drive_file',
isEnabled: () => true,
onAction: ($event) => this.getTable().addEntity($event)
},
{
name: this.translate.instant('alarm-rule.import'),
icon: 'file_upload',
isEnabled: () => true,
onAction: () => this.importCalculatedField()
}
];
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.columns.push(new DateEntityTableColumn<CalculatedFieldAlarmRule>('createdTime', 'common.created-time', this.datePipe, '150px'));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('name', 'alarm-rule.alarm-type', '33%'));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', '67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '60px',
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: '0 14px'}}, false));
this.cellActionDescriptors.push(
{
name: this.translate.instant('action.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: (event$, entity) => this.exportCalculatedField(event$, entity),
},
{
name: this.translate.instant('entity-view.events'),
icon: 'mdi:clipboard-text-clock',
isEnabled: () => true,
onAction: (_, entity) => this.openDebugEventsDialog(entity),
},
{
name: '',
nameFunction: entity => this.entityDebugSettingsService.getDebugConfigLabel(entity?.debugSettings),
icon: 'mdi:bug',
isEnabled: () => true,
iconFunction: ({ debugSettings }) => this.entityDebugSettingsService.isDebugActive(debugSettings?.allEnabledUntil) || debugSettings?.failuresEnabled ? 'mdi:bug' : 'mdi:bug-outline',
onAction: ($event, entity) => this.onOpenDebugConfig($event, entity),
},
{
name: this.translate.instant('action.edit'),
icon: 'edit',
isEnabled: () => true,
onAction: (_, entity) => this.editCalculatedField(entity),
}
);
}
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM);
}
onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
const { debugSettings = {}, id } = calculatedField;
const additionalActionConfig = {
...this.additionalDebugActionConfig,
action: () => this.openDebugEventsDialog(calculatedField)
};
if ($event) {
$event.stopPropagation();
}
const { viewContainerRef, renderer } = this.entityDebugSettingsService;
if (!viewContainerRef || !renderer) {
this.entityDebugSettingsService.viewContainerRef = this.getTable().viewContainerRef;
this.entityDebugSettingsService.renderer = this.renderer;
}
this.entityDebugSettingsService.openDebugStrategyPanel({
debugSettings,
debugConfig: {
entityType: EntityType.CALCULATED_FIELD,
additionalActionConfig,
},
onSettingsAppliedFn: settings => this.onDebugConfigChanged(id.id, settings)
}, $event.target as Element);
}
private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty)
.subscribe((res) => {
if (res) {
this.updateData();
}
});
}
private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedField> {
return this.dialog.open<AlarmRuleDialogComponent, AlarmRuleDialogData, CalculatedField>(AlarmRuleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle,
entityId: this.entityId,
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
additionalDebugActionConfig: this.additionalDebugActionConfig,
isDirty,
},
enterAnimationDuration: isDirty ? 0 : null,
})
.afterClosed()
.pipe(filter(Boolean));
}
private openDebugEventsDialog(calculatedField: CalculatedField): void {
this.dialog.open<CalculatedFieldDebugDialogComponent, CalculatedFieldDebugDialogData, null>(CalculatedFieldDebugDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
tenantId: this.tenantId,
value: calculatedField,
getTestScriptDialogFn: null,
}
})
.afterClosed()
.subscribe();
}
private exportCalculatedField($event: Event, calculatedField: CalculatedField): void {
if ($event) {
$event.stopPropagation();
}
this.importExportService.exportCalculatedField(calculatedField.id.id);
}
private importCalculatedField(): void {
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
filter(Boolean),
switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
filter(Boolean),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => this.updateData());
}
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
if (calculatedField.type === CalculatedFieldType.GEOFENCING) {
calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => {
const arg = calculatedField.configuration.zoneGroups[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
: arg;
return acc;
}, {});
} else {
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const arg = calculatedField.configuration.arguments[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
: arg;
return acc;
}, {});
}
return calculatedField;
}
private onDebugConfigChanged(id: string, debugSettings: EntityDebugSettings): void {
this.calculatedFieldsService.getCalculatedFieldById(id).pipe(
switchMap(field => this.calculatedFieldsService.saveCalculatedField({ ...field, debugSettings })),
catchError(() => of(null)),
takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData());
}
}

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

@ -0,0 +1,20 @@
<!--
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.
-->
@if (calculatedFieldsTableConfig) {
<tb-entities-table [entitiesTableConfig]="calculatedFieldsTableConfig"></tb-entities-table>
}

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

@ -0,0 +1,22 @@
/**
* 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.
*/
:host ::ng-deep {
tb-entities-table {
.mat-drawer-container {
background-color: white;
}
}
}

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

@ -0,0 +1,88 @@
///
/// 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.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
effect,
input,
Renderer2,
ViewChild,
} from '@angular/core';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitiesTableComponent } from '@home/components/entity/entities-table.component';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
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';
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
@Component({
selector: 'tb-alarm-rules-table',
templateUrl: './alarm-rules-table.component.html',
styleUrls: ['./alarm-rules-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [EntityDebugSettingsService]
})
export class AlarmRulesTableComponent {
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
active = input<boolean>();
entityId = input<EntityId>();
entityName = input<string>();
ownerId = input<EntityId>();
calculatedFieldsTableConfig: AlarmRulesTableConfig;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
private store: Store<AppState>,
private datePipe: DatePipe,
private cd: ChangeDetectorRef,
private renderer: Renderer2,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private destroyRef: DestroyRef) {
effect(() => {
if (this.active()) {
this.calculatedFieldsTableConfig = new AlarmRulesTableConfig(
this.calculatedFieldsService,
this.translate,
this.dialog,
this.datePipe,
this.entityId(),
this.store,
this.destroyRef,
this.renderer,
this.entityName(),
this.ownerId(),
this.importExportService,
this.entityDebugSettingsService,
);
this.cd.markForCheck();
}
});
}
}

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

@ -0,0 +1,196 @@
<!--
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.
-->
<form [formGroup]="conditionFormGroup" (ngSubmit)="save()">
<mat-toolbar color="primary">
<h2>{{ (readonly ? 'alarm-rule.alarm-rule-condition' : 'alarm-rule.edit-alarm-rule-condition') | translate }}</h2>
<span class="flex-1"></span>
<div formGroupName="expression" class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;">
<tb-toggle-select formControlName="type" appearance="fill-invert" selectMediaBreakpoint="xs">
<tb-toggle-option [value]="AlarmRuleExpressionType.SIMPLE">{{ 'alarm-rule.expression-type.simple' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="AlarmRuleExpressionType.TBEL">{{ 'alarm-rule.expression-type.tbel' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<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="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>
<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>
</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>
<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>
</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>
<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>
</div>
}
</section>
</div>
</fieldset>
</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>
</div>
<ng-template #staticValueTemplate let-type="type" let-groupName="groupName">
<div class="flex flex-1 flex-row items-center gap-2" [formGroupName]="groupName">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input type="number" matInput
step="1" min="1" max="2147483647"
formControlName="staticValue">
<mat-label>{{ defaultValuePlaceholder | translate }}</mat-label>
@if (conditionFormGroup.get(groupName).get('staticValue').hasError('required')) {
<mat-error>{{ defaultValueRequiredError | translate }}</mat-error>
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('min')) {
<mat-error>{{ defaultValueRangeError | translate }}</mat-error>
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('max')) {
<mat-error>{{ defaultValueRangeError | translate }}</mat-error>
} @else if (conditionFormGroup.get(groupName).get('staticValue').hasError('pattern')) {
<mat-error>{{ defaultValuePatternError | translate }}</mat-error>
}
</mat-form-field>
</div>
</ng-template>
<ng-template #dynamicValueTemplate let-type="type" let-groupName="groupName">
<ng-container [formGroupName]="groupName">
<mat-form-field class="flex flex-1" appearance="outline">
<mat-label translate>alarm-rule.value-argument</mat-label>
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get(groupName).get('dynamicValueArgument').hasError('required')">
{{ 'calculated-fields.hint.argument-name-required' | translate }}
</mat-error>
</mat-form-field>
</ng-container>
</ng-template>
</form>

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

@ -0,0 +1,244 @@
///
/// 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.
///
import { Component, Inject, OnInit, SkipSelf } 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 { 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";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import {
CalculatedFieldArgument,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights
} from "@shared/models/calculated-field.models";
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmRuleCondition;
arguments?: Record<string, CalculatedFieldArgument>;
}
@Component({
selector: 'tb-cf-alarm-rule-condition-dialog',
templateUrl: './cf-alarm-rule-condition-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmRuleConditionDialogComponent}],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarmRuleConditionDialogComponent, AlarmRuleCondition>
implements OnInit, ErrorStateMatcher {
AlarmRuleExpressionType = AlarmRuleExpressionType;
timeUnits = Object.values(TimeUnit);
timeUnitTranslations = timeUnitTranslationMap;
alarmConditionTypes = Object.values(AlarmRuleConditionType);
AlarmConditionType = AlarmRuleConditionType;
alarmConditionTypeTranslation = AlarmRuleConditionTypeTranslationMap;
readonly = this.data.readonly;
condition = this.data.condition;
conditionFormGroup: FormGroup<FormControlsFrom<AlarmRuleCondition>>;
submitted = false;
readonly scriptLanguage = ScriptLanguage;
defaultValuePlaceholder = '';
defaultValueRequiredError = '';
defaultValueRangeError = '';
defaultValuePatternError = '';
durationDynamicMode = !!this.condition?.value?.dynamicValueArgument;
repeatingDynamicMode = !!this.condition?.count?.dynamicValueArgument;
ComplexOperation = ComplexOperation;
complexOperationTranslationMap = complexOperationTranslationMap;
functionArgs: Array<string>;
argumentsEditorCompleter: TbEditorCompleter;
argumentsHighlightRules: AceHighlightRules;
arguments = this.data.arguments;
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) {
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.conditionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type) => {
this.updateValidators(type, true);
});
this.conditionFormGroup.get('expression.type').valueChanges.pipe(
takeUntilDestroyed()
).subscribe((type) => {
this.updateExpressionTypeValidator(type);
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
});
this.updateValidators(this.conditionFormGroup.get('type').value ?? AlarmRuleConditionType.SIMPLE);
this.updateExpressionTypeValidator(this.condition?.expression?.type ?? 'SIMPLE');
}
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';
if (dynamicValue) {
this.conditionFormGroup.get(`${control}.staticValue`).disable({emitEvent: false});
this.conditionFormGroup.get(`${control}.dynamicValueArgument`).enable({emitEvent: false});
} else {
this.conditionFormGroup.get(`${control}.staticValue`).enable({emitEvent: false});
this.conditionFormGroup.get(`${control}.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();
} else {
this.conditionFormGroup.get(`expression.expression`).enable();
this.conditionFormGroup.get(`expression.filters`).disable();
}
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.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.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();
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 {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
this.condition = this.conditionFormGroup.value as AlarmRuleCondition;
this.dialogRef.close(this.condition);
}
}

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

@ -0,0 +1,52 @@
<!--
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="tb-alarm-rule-condition flex flex-col gap-4 min-w-0" [formGroup]="alarmRuleConditionFormGroup">
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.condition' | translate }}</div>
<button [disabled]="disabled"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button [color]="conditionSet() ? 'primary' : 'warn'"
(click)="openFilterDialog($event)">
<div class="flex items-center gap-2 justify-between">
<tb-alarm-rule-filter-text [alarmRuleExpression]="alarmRuleConditionFormGroup.get('expression').value"
[arguments]="arguments"
class="flex-1"
[nowrap]="true"
[specText]="specText"
required
addFilterPrompt="{{'alarm-rule.enter-alarm-rule-condition-prompt' | translate}}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
</div>
</button>
</div>
<div class="tb-form-row column-xs">
<div class="min-w-40 xs:min-w-fit">{{ 'alarm-rule.schedule-title' | translate }}</div>
<button [disabled]="disabled"
type="button"
class="tb-alarm-rule-condition-button"
mat-stroked-button color="primary"
(click)="openScheduleDialog($event)">
<div class="flex items-center gap-2">
<span class="tb-alarm-rule-condition-label" tbTruncateWithTooltip [innerHTML]="scheduleText"></span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
</div>
</button>
</div>
</div>

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

@ -0,0 +1,37 @@
/**
* 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.
*/
:host {
display: flex;
flex: 1;
.tb-alarm-rule-condition {
display: flex;
flex: 1;
&-button {
--mat-outlined-button-horizontal-padding: 3px 0px 12px;
display: block;
width: 100%;
}
&-label {
display: block;
text-align: start;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

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

@ -0,0 +1,283 @@
///
/// 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.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
Validator,
Validators
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import {
dayOfWeekTranslations,
getAlarmScheduleRangeText,
utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { TimeUnit } from '@shared/models/time/time.models';
import {
CfAlarmRuleConditionDialogComponent,
CfAlarmRuleConditionDialogData
} from "@home/components/alarm-rules/cf-alarm-rule-condition-dialog.component";
import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleSchedule,
AlarmRuleScheduleType
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import {
AlarmRuleScheduleDialogData,
CfAlarmScheduleDialogComponent
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
@Component({
selector: 'tb-cf-alarm-rule-condition',
templateUrl: './cf-alarm-rule-condition.component.html',
styleUrls: ['./cf-alarm-rule-condition.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CfAlarmRuleConditionComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CfAlarmRuleConditionComponent),
multi: true,
}
]
})
export class CfAlarmRuleConditionComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
disabled: boolean;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'],
expression: [null, Validators.required],
schedule: [null],
});
specText = '';
scheduleText = '';
private modelValue: AlarmRuleCondition;
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef,
private translate: TranslateService) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmRuleConditionFormGroup.disable({emitEvent: false});
} else {
this.alarmRuleConditionFormGroup.enable({emitEvent: false});
}
}
writeValue(value: AlarmRuleCondition): void {
this.modelValue = value;
this.updateConditionInfo();
}
public conditionSet() {
return this.modelValue && (this.modelValue.expression.expression || this.modelValue.expression.filters);
}
public validate(c: UntypedFormControl) {
return this.conditionSet() ? null : {
alarmRuleCondition: {
valid: false,
},
};
}
public openFilterDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<CfAlarmRuleConditionDialogComponent, CfAlarmRuleConditionDialogData,
AlarmRuleCondition>(CfAlarmRuleConditionDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
arguments: this.arguments
}
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue = {...this.modelValue, ...result};
this.updateModel();
this.updateSpecText();
this.cd.detectChanges();
}
});
}
private updateConditionInfo() {
this.alarmRuleConditionFormGroup.patchValue(
{
type: this.modelValue?.type,
expression: this.modelValue?.expression,
schedule: this.modelValue?.schedule,
}, {emitEvent: false}
);
this.updateScheduleText();
this.updateSpecText();
}
private updateSpecText() {
this.specText = '';
if (this.modelValue && this.modelValue.type) {
const type = this.modelValue.type;
switch (type) {
case AlarmRuleConditionType.SIMPLE:
break;
case AlarmRuleConditionType.DURATION:
let duringText = '';
switch (this.modelValue.unit) {
case TimeUnit.SECONDS:
duringText = this.translate.instant('timewindow.seconds', {seconds: this.modelValue.value.staticValue});
break;
case TimeUnit.MINUTES:
duringText = this.translate.instant('timewindow.minutes', {minutes: this.modelValue.value.staticValue});
break;
case TimeUnit.HOURS:
duringText = this.translate.instant('timewindow.hours', {hours: this.modelValue.value.staticValue});
break;
case TimeUnit.DAYS:
duringText = this.translate.instant('timewindow.days', {days: this.modelValue.value.staticValue});
break;
}
if (this.modelValue.value.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-during-dynamic', {
attribute: `${this.modelValue.value.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-during', {
during: duringText
});
}
break;
case AlarmRuleConditionType.REPEATING:
if (this.modelValue.count.dynamicValueArgument) {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times-dynamic', {
attribute: `${this.modelValue.count.dynamicValueArgument}`
});
} else {
this.specText = this.translate.instant('alarm-rule.condition-repeat-times',
{count: this.modelValue.count.staticValue});
}
break;
}
}
}
private updateModel() {
this.updateConditionInfo();
this.propagateChange(this.modelValue);
}
public openScheduleDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<CfAlarmScheduleDialogComponent, AlarmRuleScheduleDialogData,
AlarmRuleSchedule>(CfAlarmScheduleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
readonly: this.disabled,
alarmSchedule: this.disabled ? this.modelValue?.schedule : deepClone(this.modelValue?.schedule),
arguments: this.arguments
}
}).afterClosed().subscribe((result) => {
if (result) {
this.modelValue.schedule = result;
this.propagateChange(this.modelValue);
this.updateScheduleText();
this.cd.detectChanges();
}
});
}
private updateScheduleText() {
let schedule = this.modelValue?.schedule;
this.scheduleText = '';
if (isDefinedAndNotNull(schedule)) {
if (schedule.dynamicValueArgument) {
this.scheduleText = this.translate.instant('alarm-rule.value-argument') + ': ' + schedule?.dynamicValueArgument
} else {
switch (schedule.staticValue.type) {
case AlarmRuleScheduleType.ANY_TIME:
this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time');
break;
case AlarmRuleScheduleType.SPECIFIC_TIME:
for (const day of schedule.staticValue.daysOfWeek) {
if (this.scheduleText.length) {
this.scheduleText += ', ';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[day - 1]);
}
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(schedule.staticValue.startsOn),
utcTimestampToTimeOfDay(schedule.staticValue.endsOn)) + '</b>';
break;
case AlarmRuleScheduleType.CUSTOM:
for (const item of schedule.staticValue.items) {
if (item.enabled) {
if (this.scheduleText.length) {
this.scheduleText += ', ';
}
this.scheduleText += this.translate.instant(dayOfWeekTranslations[item.dayOfWeek - 1]);
this.scheduleText += ' <b>' + getAlarmScheduleRangeText(utcTimestampToTimeOfDay(item.startsOn),
utcTimestampToTimeOfDay(item.endsOn)) + '</b>';
}
}
break;
}
}
}
if (!this.scheduleText.length) {
this.scheduleText = this.translate.instant('alarm-rule.schedule.any-time');
}
}
}

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

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

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

@ -0,0 +1,35 @@
/**
* 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.
*/
:host {
min-width: 0;
.row {
margin-top: 1em;
}
.tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px;
&.title {
opacity: 0.7;
overflow: visible;
}
}
.tb-alarm-rule-details {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
}
}

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

@ -0,0 +1,153 @@
///
/// 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.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
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';
import { AlarmRule } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import {
AlarmRuleDetailsDialogComponent,
AlarmRuleDetailsDialogData
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
@Component({
selector: 'tb-cf-alarm-rule',
templateUrl: './cf-alarm-rule.component.html',
styleUrls: ['./cf-alarm-rule.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CfAlarmRuleComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CfAlarmRuleComponent),
multi: true,
}
]
})
export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Validator {
@Input()
disabled: boolean;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
arguments: Record<string, CalculatedFieldArgument>;
private modelValue: AlarmRule;
alarmRuleFormGroup= this.fb.group({
condition: [null, [Validators.required]],
alarmDetails: [null],
dashboardId: [null]
});
private propagateChange = (v: any) => { };
constructor(private dialog: MatDialog,
private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.alarmRuleFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmRuleFormGroup.disable({emitEvent: false});
} else {
this.alarmRuleFormGroup.enable({emitEvent: false});
}
}
writeValue(value: AlarmRule): void {
this.modelValue = value;
const model = this.modelValue ? {
...this.modelValue,
dashboardId: this.modelValue.dashboardId?.id
} : null;
this.alarmRuleFormGroup.patchValue(model, {emitEvent: false});
}
public openEditDetailsDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<AlarmRuleDetailsDialogComponent, AlarmRuleDetailsDialogData,
string>(AlarmRuleDetailsDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
alarmDetails: this.alarmRuleFormGroup.get('alarmDetails').value,
readonly: this.disabled
}
}).afterClosed().subscribe((alarmDetails) => {
if (isDefinedAndNotNull(alarmDetails)) {
this.alarmRuleFormGroup.patchValue({alarmDetails});
}
});
}
public validate(c: UntypedFormControl) {
return (!this.required && !this.modelValue || this.alarmRuleFormGroup.valid) ? null : {
alarmRule: {
valid: false,
},
};
}
private updateModel() {
const value = this.alarmRuleFormGroup.value;
this.modelValue = {...this.modelValue, ...value, dashboardId: value.dashboardId ? new DashboardId(value.dashboardId) : null};
this.propagateChange(this.modelValue);
}
}

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

@ -0,0 +1,24 @@
/**
* 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.
*/
:host {
form {
width: 900px;
max-width: 100%;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
}
}

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

@ -0,0 +1,54 @@
<!--
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.
-->
<form [formGroup]="alarmScheduleFormGroup" (ngSubmit)="save()">
<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>
<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>
<tb-cf-alarm-schedule [arguments]="arguments"
[settingsMode]="settingsMode"
formControlName="alarmSchedule">
</tb-cf-alarm-schedule>
</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) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,92 @@
///
/// 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.
///
import { Component, Inject, OnInit, SkipSelf } 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 { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } 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";
export interface AlarmRuleScheduleDialogData {
readonly: boolean;
alarmSchedule: AlarmRuleSchedule;
arguments: Record<string, CalculatedFieldArgument>;
}
@Component({
selector: 'tb-cf-alarm-schedule-dialog',
templateUrl: './cf-alarm-schedule-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: CfAlarmScheduleDialogComponent}],
styleUrls: ['./cf-alarm-rules-dialog.component.scss'],
})
export class CfAlarmScheduleDialogComponent extends DialogComponent<CfAlarmScheduleDialogComponent, AlarmRuleSchedule>
implements OnInit, ErrorStateMatcher {
readonly = this.data.readonly;
alarmSchedule = this.data.alarmSchedule;
arguments = this.data.arguments;
alarmScheduleFormGroup: UntypedFormGroup;
submitted = false;
settingsMode: 'static' | 'dynamic' = 'static';
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) {
super(store, router, dialogRef);
this.alarmScheduleFormGroup = this.fb.group({
alarmSchedule: [this.alarmSchedule]
});
this.settingsMode = this.alarmSchedule?.dynamicValueArgument ? 'dynamic' : 'static';
if (this.readonly) {
this.alarmScheduleFormGroup.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);
}
}

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

@ -0,0 +1,124 @@
<!--
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.
-->
<section [formGroup]="alarmScheduleForm" class="flex flex-col">
@if (settingsMode === 'static') {
<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>
</mat-select>
<mat-error *ngIf="alarmScheduleForm.get('staticValue.type').hasError('required')">
{{ 'alarm-rule.schedule-type-required' | translate }}
</mat-error>
</mat-form-field>
</ng-container>
@if (alarmScheduleForm.get('staticValue.type').value !== alarmScheduleType.ANY_TIME) {
<div class="tb-form-panel no-padding no-border">
<div formGroupName="staticValue">
<tb-timezone-select
appearance="outline"
userTimezoneByDefault
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">
<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]"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</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>
</div>
</div>
}
} @else {
<div class="flex flex-row items-center gap-2">
<mat-form-field class="flex-1" appearance="outline">
<mat-label translate>alarm-rule.value-argument</mat-label>
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<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>
</mat-form-field>
<div matSuffix style="height: 56px"
[tb-help-popup]="'alarm-rule/alarm_rule_schedule_format'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
}
</section>

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

@ -0,0 +1,325 @@
///
/// 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.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
dayOfWeekTranslations,
getAlarmScheduleRangeText,
timeOfDayToUTCTimestamp,
utcTimestampToTimeOfDay
} from '@shared/models/device.models';
import { isDefined } from '@core/utils';
import { getDefaultTimezone } from '@shared/models/time/time.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
AlarmRuleSchedule,
AlarmRuleScheduleType,
AlarmRuleScheduleTypeTranslationMap
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { MatChipSelectionChange } from "@angular/material/chips";
@Component({
selector: 'tb-cf-alarm-schedule',
templateUrl: './cf-alarm-schedule.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CfAlarmScheduleComponent),
multi: true
}, {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CfAlarmScheduleComponent),
multi: true
}]
})
export class CfAlarmScheduleComponent implements ControlValueAccessor, Validator, OnInit {
@Input()
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();
}
}
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],
items: this.fb.array(Array.from({length: 7}, (value, i) => this.defaultItemsScheduler(i)), this.validateItems),
}),
dynamicValueArgument: [null, Validators.required]
});
alarmScheduleTypes = Object.keys(AlarmRuleScheduleType);
alarmScheduleType = AlarmRuleScheduleType;
alarmScheduleTypeTranslate = AlarmRuleScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations;
allDays = Array(7).fill(0).map((x, i) => i);
private modelValue: AlarmRuleSchedule;
private defaultItems = Array.from({length: 7}, (value, i) => ({
enabled: true,
dayOfWeek: i + 1
}));
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
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});
this.alarmScheduleForm.get('dynamicValueArgument').patchValue(null, {emitEvent: false});
this.updateValidators(type);
this.alarmScheduleForm.updateValueAndValidity();
});
this.alarmScheduleForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
this.alarmScheduleForm.get('staticValue.items').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((items) => {
items.forEach((item, index) => this.disabledSelectedTime(item.enabled, index, false))
});
}
validateItems(control: AbstractControl): ValidationErrors | null {
const items: any[] = control.value;
if (!items || !items.length || !items.find(v => v.enabled === true)) {
return {
dayOfWeeks: true
};
}
return null;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmScheduleForm.disable({emitEvent: false});
} else {
this.updateModeValidators(this.settingsMode);
}
}
writeValue(value: AlarmRuleSchedule): void {
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({
staticValue: {
type: this.modelValue.staticValue.type,
timezone: this.modelValue.staticValue.timezone,
daysOfWeek: this.modelValue.staticValue.daysOfWeek,
startsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.startsOn),
endsOn: utcTimestampToTimeOfDay(this.modelValue.staticValue.endsOn),
},
}, {emitEvent: false});
break;
case AlarmRuleScheduleType.CUSTOM:
if (this.modelValue?.dynamicValueArgument) {
this.alarmScheduleForm.patchValue({
staticValue: {
type: this.modelValue.staticValue.type,
},
}, {emitEvent: false});
} else if (this.modelValue.staticValue?.items) {
const alarmDays = [];
this.modelValue.staticValue.items
.sort((a, b) => a.dayOfWeek - b.dayOfWeek)
.forEach((item, index) => {
this.disabledSelectedTime(item.enabled, index);
alarmDays.push({
enabled: item.enabled,
startsOn: utcTimestampToTimeOfDay(item.startsOn),
endsOn: utcTimestampToTimeOfDay(item.endsOn)
});
});
this.alarmScheduleForm.patchValue({
staticValue: {
type: this.modelValue.staticValue.type,
timezone: this.modelValue.staticValue.timezone,
items: alarmDays,
},
}, {emitEvent: false});
}
break;
default:
this.alarmScheduleForm.patchValue(this.modelValue || undefined, {emitEvent: false});
}
this.updateValidators(this.modelValue.staticValue.type);
}
this.updateModeValidators(this.settingsMode);
}
}
validate(control: UntypedFormGroup): ValidationErrors | null {
return this.alarmScheduleForm.valid ? null : {
alarmScheduler: {
valid: false
}
};
}
private updateModeValidators(mode: 'static' | 'dynamic') {
if (mode === 'static') {
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});
}
}
private updateValidators(type: AlarmRuleScheduleType){
switch (type){
case AlarmRuleScheduleType.ANY_TIME:
this.alarmScheduleForm.get('staticValue.timezone').disable({emitEvent: false});
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').disable({emitEvent: false});
break;
case AlarmRuleScheduleType.SPECIFIC_TIME:
this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.daysOfWeek').enable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.startsOn').enable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.endsOn').enable({emitEvent: false});
this.alarmScheduleForm.get('staticValue.items').disable({emitEvent: false});
break;
case AlarmRuleScheduleType.CUSTOM:
this.alarmScheduleForm.get('staticValue.timezone').enable({emitEvent: false});
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});
break;
}
}
private updateModel() {
const value = this.alarmScheduleForm.value as AlarmRuleSchedule;
if (this.settingsMode === 'static') {
if (isDefined(value.staticValue.startsOn) && value.staticValue.startsOn !== 0) {
value.staticValue.startsOn = timeOfDayToUTCTimestamp(value.staticValue.startsOn);
}
if (isDefined(value.staticValue.endsOn) && value.staticValue.endsOn !== 0) {
value.staticValue.endsOn = timeOfDayToUTCTimestamp(value.staticValue.endsOn);
}
if (isDefined(value.staticValue.items)){
value.staticValue.items = this.alarmScheduleForm.getRawValue().staticValue.items;
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);
}
}
private defaultItemsScheduler(index): UntypedFormGroup {
return this.fb.group({
enabled: [true],
dayOfWeek: [index + 1],
startsOn: [0, Validators.required],
endsOn: [0, Validators.required]
});
}
changeCustomScheduler($event: MatChipSelectionChange, index: number) {
const value = $event.selected;
this.disabledSelectedTime(value, index, true);
}
private disabledSelectedTime(enable: boolean, index: number, emitEvent = false) {
if (enable) {
this.itemsSchedulerForm.at(index).get('startsOn').enable({emitEvent: false});
this.itemsSchedulerForm.at(index).get('endsOn').enable({emitEvent});
} else {
this.itemsSchedulerForm.at(index).get('startsOn').disable({emitEvent: false});
this.itemsSchedulerForm.at(index).get('endsOn').disable({emitEvent});
}
}
getSchedulerRangeText(control: UntypedFormGroup | AbstractControl): string {
return getAlarmScheduleRangeText(control.get('startsOn').value, control.get('endsOn').value);
}
get itemsSchedulerForm(): UntypedFormArray {
return this.alarmScheduleForm.get('staticValue.items') as UntypedFormArray;
}
get argumentsList(): Array<string> {
return this.arguments ? Object.keys(this.arguments): [];
}
}

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

@ -0,0 +1,80 @@
<!--
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.
-->
<!--
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="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">
<mat-select formControlName="severity"
required
placeholder="{{ 'alarm-rule.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], index)">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" required class="flex-1">
</tb-cf-alarm-rule>
</div>
<button *ngIf="!disabled"
class="button-icon"
mat-icon-button
type="button"
(click)="removeCreateAlarmRule(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled">
<span translate class="flex items-center justify-center tb-prompt required" style="margin: 16px 0">
alarm-rule.add-create-alarm-rule-prompt
</span>
</div>
<div *ngIf="!disabled">
<button mat-stroked-button color="primary"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'alarm-rule.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
{{ 'alarm-rule.add-create-alarm-rule' | translate }}
</button>
</div>
</div>

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

@ -0,0 +1,36 @@
/**
* 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.
*/
:host {
.create-alarm-rule {
border: 1px solid rgba(0, 0, 0, .12);
border-left-width: 4px;
border-radius: 4px;
padding: 8px;
min-width: 0;
}
}
:host ::ng-deep {
.mat-mdc-form-field.severity {
.mat-mdc-form-field-infix {
width: 160px;
}
}
.button-icon {
color: rgba(0, 0, 0, 0.38);
min-width: 40px;
}
}

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

@ -0,0 +1,192 @@
///
/// 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.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
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";
@Component({
selector: 'tb-create-cf-alarm-rules',
templateUrl: './create-cf-alarm-rules.component.html',
styleUrls: ['./create-cf-alarm-rules.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CreateCfAlarmRulesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CreateCfAlarmRulesComponent),
multi: true,
}
]
})
export class CreateCfAlarmRulesComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
alarmSeverityTranslationMap = alarmSeverityTranslations;
AlarmSeverityNotificationColors = AlarmSeverityNotificationColors;
@Input()
disabled: boolean;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
createAlarmRulesFormGroup: UntypedFormGroup;
private usedSeverities: AlarmSeverity[] = [];
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => { };
constructor(private fb: UntypedFormBuilder) {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
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;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else {
this.createAlarmRulesFormGroup.enable({emitEvent: false});
}
}
writeValue(createAlarmRules: {[severity: string]: AlarmRule}): void {
const createAlarmRulesControls: Array<AbstractControl> = [];
if (createAlarmRules) {
Object.keys(createAlarmRules).forEach((severity) => {
const createAlarmRule = createAlarmRules[severity];
if (severity === 'empty') {
severity = null;
}
createAlarmRulesControls.push(this.fb.group({
severity: [severity, Validators.required],
alarmRule: [createAlarmRule, Validators.required]
}));
});
}
this.createAlarmRulesFormGroup.setControl('createAlarmRules', this.fb.array(createAlarmRulesControls), {emitEvent: false});
if (this.disabled) {
this.createAlarmRulesFormGroup.disable({emitEvent: false});
} else {
this.createAlarmRulesFormGroup.enable({emitEvent: false});
}
this.updateUsedSeverities();
if (!this.disabled && !this.createAlarmRulesFormGroup.valid) {
this.updateModel();
}
}
public removeCreateAlarmRule(index: number) {
(this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray).removeAt(index);
}
public addCreateAlarmRule() {
const createAlarmRulesArray = this.createAlarmRulesFormGroup.get('createAlarmRules') as UntypedFormArray;
createAlarmRulesArray.push(this.fb.group({
severity: [this.getFirstUnusedSeverity(), Validators.required],
alarmRule: [null, Validators.required]
}));
this.createAlarmRulesFormGroup.updateValueAndValidity();
if (!this.createAlarmRulesFormGroup.valid) {
this.updateModel();
}
}
private getFirstUnusedSeverity(): AlarmSeverity {
for (const severityKey of Object.keys(AlarmSeverity)) {
const severity = AlarmSeverity[severityKey];
if (this.usedSeverities.indexOf(severity) === -1) {
return severity;
}
}
return null;
}
public validate(c: UntypedFormControl) {
return (this.createAlarmRulesFormGroup.valid) ? null : {
createAlarmRules: {
valid: false,
},
};
}
public isDisabledSeverity(severity: AlarmSeverity, index: number): boolean {
const usedIndex = this.usedSeverities.indexOf(severity);
return usedIndex > -1 && usedIndex !== index;
}
private updateUsedSeverities() {
this.usedSeverities = [];
const value: {severity: string, alarmRule: AlarmRule}[] = 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} = {};
value.forEach(v => createAlarmRules[v.severity] = v.alarmRule);
this.updateUsedSeverities();
this.propagateChange(createAlarmRules);
}
}

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

@ -0,0 +1,59 @@
<!--
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.
-->
<form [formGroup]="complexFilterFormGroup" (ngSubmit)="save()" style="width: 1200px;">
<mat-toolbar color="primary">
<h2 translate>filter.complex-filter</h2>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</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>
</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">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,111 @@
///
/// 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.
///
import { Component, Inject, OnInit, SkipSelf } 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 { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType
} from '@shared/models/query/query.models';
import { ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { FormControlsFrom } from "@shared/models/tenant.model";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
export interface AlarmRuleComplexFilterPredicateDialogData {
complexPredicate: ComplexAlarmRuleFilterPredicate;
isAdd: boolean;
valueType: EntityKeyValueType;
arguments: Record<string, CalculatedFieldArgument>;
}
@Component({
selector: 'tb-alarm-rule-complex-filter-predicate-dialog',
templateUrl: './alarm-rule-complex-filter-predicate-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleComplexFilterPredicateDialogComponent}],
styleUrls: []
})
export class AlarmRuleComplexFilterPredicateDialogComponent extends
DialogComponent<AlarmRuleComplexFilterPredicateDialogComponent, ComplexAlarmRuleFilterPredicate>
implements OnInit, ErrorStateMatcher {
complexFilterFormGroup: FormGroup<FormControlsFrom<ComplexAlarmRuleFilterPredicate>>;
complexOperations = Object.keys(ComplexOperation);
complexOperationEnum = ComplexOperation;
complexOperationTranslations = complexOperationTranslationMap;
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) {
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;
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
if (this.complexFilterFormGroup.valid) {
const predicate: ComplexAlarmRuleFilterPredicate = this.complexFilterFormGroup.value as ComplexAlarmRuleFilterPredicate;
predicate.type = FilterPredicateType.COMPLEX;
this.dialogRef.close(predicate);
}
}
}

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

@ -0,0 +1,101 @@
<!--
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.
-->
<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>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" 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">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.value-argument</mat-label>
<mat-select formControlName="argument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument" [disabled]="argumentInUse(argument)" >{{ argument }}</mat-option>
}
</mat-select>
@if (filterFormGroup.get('argument').touched && filterFormGroup.get('argument').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate"
class="tb-error !block">
warning
</mat-icon>
}
</mat-form-field>
<mat-form-field class="flex-1 tb-value-type" appearance="outline" subscriptSizing="dynamic" hideRequiredMarker>
<mat-label translate>filter.value-type.value-type</mat-label>
<mat-select formControlName="valueType">
<mat-select-trigger>
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }}</span>
</mat-select-trigger>
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType">
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
</mat-select>
<mat-error *ngIf="filterFormGroup.get('valueType').hasError('required')">
{{ 'filter.value-type-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</section>
<section class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.filter' | 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-predicate-list [valueType]="filterFormGroup.get('valueType').value"
[operation]="filterFormGroup.get('operation').value"
[arguments]="arguments"
formControlName="predicates">
</tb-alarm-rule-filter-predicate-list>
</section>
</fieldset>
</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">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
</div>
</form>

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

@ -0,0 +1,28 @@
/**
* 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.
*/
:host ::ng-deep {
.mat-mdc-form-field.tb-value-type {
mat-select-trigger {
.mat-icon {
vertical-align: middle;
margin-right: 8px;
svg {
vertical-align: initial;
}
}
}
}
}

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

@ -0,0 +1,153 @@
///
/// 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.
///
import { Component, Inject, OnDestroy, SkipSelf } 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 { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypesMap
} 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";
export interface AlarmRuleFilterDialogData {
filter: AlarmRuleFilter;
isAdd: boolean;
arguments: Record<string, CalculatedFieldArgument>;
usedArguments: Array<string>;
}
@Component({
selector: 'tb-alarm-rule-filter-dialog',
templateUrl: './alarm-rule-filter-dialog.component.html',
providers: [{provide: ErrorStateMatcher, useExisting: AlarmRuleFilterDialogComponent}],
styleUrls: ['./alarm-rule-filter-dialog.component.scss']
})
export class AlarmRuleFilterDialogComponent extends DialogComponent<AlarmRuleFilterDialogComponent, AlarmRuleFilter>
implements OnDestroy, ErrorStateMatcher {
private destroy$ = new Subject<void>();
filterFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilter>>;
entityKeyValueTypesKeys = Object.keys(EntityKeyValueType);
entityKeyValueTypeEnum = EntityKeyValueType;
entityKeyValueTypes = entityKeyValueTypesMap;
complexOperationTranslationMap = complexOperationTranslationMap;
ComplexOperation = ComplexOperation;
submitted = false;
searchText = '';
arguments = this.data.arguments;
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) {
super(store, router, dialogRef);
this.filterFormGroup = this.fb.group(
{
argument: [this.data.filter.argument, [Validators.required]],
valueType: [this.data.filter.valueType ?? EntityKeyValueType.STRING, [Validators.required]],
predicates: [this.data.filter.predicates, [Validators.required]],
operation: [this.data.filter.operation ?? ComplexOperation.AND]
}
);
this.filterFormGroup.get('valueType').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((valueType: EntityKeyValueType) => {
const prevValueType: EntityKeyValueType = this.filterFormGroup.value.valueType;
const predicates: AlarmRuleFilterPredicate[] = this.filterFormGroup.get('predicates').value;
if (prevValueType && prevValueType !== valueType) {
if (predicates && predicates.length) {
this.dialogs.confirm(this.translate.instant('filter.key-value-type-change-title'),
this.translate.instant('filter.key-value-type-change-message')).subscribe(
(result) => {
if (result) {
this.filterFormGroup.get('predicates').setValue([]);
} else {
this.filterFormGroup.get('valueType').setValue(prevValueType, {emitEvent: false});
}
}
);
}
}
});
}
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) : [];
}
}

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

@ -0,0 +1,81 @@
<!--
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.
-->
<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>
</div>
@if (index) {
<mat-divider></mat-divider>
}
</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>

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

@ -0,0 +1,50 @@
/**
* 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.
*/
:host {
.filter-title {
padding: 12px 0;
font-size: 14px;
font-weight: 500;
}
.filter-list {
overflow: auto;
max-height: 300px;
.no-data-found {
height: 50px;
}
&-divider {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
}
.filters-operation {
display: flex;
justify-content: center;
margin-top: -14px;
&-container {
background-color: white;
}
&-label {
font-weight: 500;
color: #00695C;
padding: 0 8px;
border-radius: 4px;
border: 1px solid rgba(#00695C, 0.32);
background-color: rgba(#00695C, 0.04);
}
}
}

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

@ -0,0 +1,195 @@
///
/// 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.
///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import {
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType
} 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";
@Component({
selector: 'tb-alarm-rule-filter-list',
templateUrl: './alarm-rule-filter-list.component.html',
styleUrls: ['./alarm-rule-filter-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmRuleFilterListComponent),
multi: true
}
]
})
export class AlarmRuleFilterListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input() operation: ComplexOperation = ComplexOperation.AND;
filterListFormGroup: UntypedFormGroup;
filtersControl: FormControl;
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);
this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get filtersFormArray(): UntypedFormArray {
return this.filterListFormGroup.get('filters') as UntypedFormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
validate(): ValidationErrors | null {
return this.filterListFormGroup.valid && this.filtersControl.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]));
}
}
this.filterListFormGroup.setControl('filters', this.fb.array(keyFilterControls), {emitEvent: false});
}
this.filtersControl.patchValue(filters, {emitEvent: false});
}
public removeFilter(index: number) {
(this.filterListFormGroup.get('filters') as UntypedFormArray).removeAt(index);
}
public addFilter() {
const filtersFormArray = this.filterListFormGroup.get('filters') as UntypedFormArray;
this.openFilterDialog(null).subscribe(result => {
if (result) {
filtersFormArray.push(this.fb.control(result, [Validators.required]));
}
});
}
public editFilter(index: number) {
const filter: AlarmRuleFilter =
(this.filterListFormGroup.get('filters') as UntypedFormArray).at(index).value;
this.openFilterDialog(filter).subscribe(result => {
if (result) {
(this.filterListFormGroup.get('filters') as UntypedFormArray).at(index).patchValue(result);
}
});
}
private openFilterDialog(filter?: AlarmRuleFilter): Observable<AlarmRuleFilter> {
const isAdd = !filter;
if (!filter) {
filter = {
argument: null,
valueType: EntityKeyValueType.STRING,
operation: ComplexOperation.AND,
predicates: []
};
}
return this.dialog.open<AlarmRuleFilterDialogComponent, AlarmRuleFilterDialogData,
AlarmRuleFilter>(AlarmRuleFilterDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
filter: filter ? deepClone(filter) : null,
isAdd,
arguments: this.arguments,
usedArguments: this.getUsedArguments
}
}).afterClosed();
}
get getUsedArguments(): Array<string> {
const filters = this.filterListFormGroup.get('filters').value ?? [];
return filters.length ? filters.map((filter: AlarmRuleFilter) => filter.argument) : filters;
}
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);
}
}
}

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

@ -0,0 +1,84 @@
<!--
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.
-->
<section class="flex flex-col" [formGroup]="filterListFormGroup">
<div class="tb-form-panel no-padding no-gap stroked">
<div class="flex flex-row">
<div class="flex flex-full flex-row items-center justify-start gap-2">
<div class="flex flex-1 flex-row gap-4 pl-2">
<label translate class="filter-title max-w-30% flex-full">alarm-rule.operation</label>
<label translate class="filter-title max-w-20% flex-full xs:hidden">alarm-rule.value-source</label>
<label translate class="filter-title max-w-50% flex-full">alarm-rule.value</label>
</div>
<span [class.!hidden]="disabled" style="min-width: 40px;">&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">
@if (index) {
<div class="filters-operation-container">
<span class="filters-operation-label">{{ complexOperationTranslations.get(operation) | translate }}</span>
</div>
}
<div class="flex flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start">
<tb-alarm-rule-filter-predicate
class="flex-1"
[arguments]="arguments"
[valueType]="valueType"
[formControl]="predicateControl">
</tb-alarm-rule-filter-predicate>
<button mat-icon-button color="primary"
[class.!hidden]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
}
<span [class.!hidden]="!!predicatesFormArray.length"
[class.disabled]="disabled"
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>
</section>

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

@ -0,0 +1,50 @@
/**
* 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.
*/
:host {
.filter-title {
padding: 12px 8px;
font-size: 14px;
font-weight: 500;
}
.predicate-list {
.no-data-found {
height: 50px;
}
}
.key-filter-list-divider {
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.filters-operation {
display: flex;
justify-content: center;
margin-top: -14px;
&-container {
position: absolute;
top: -12px;
left: 10px;
background-color: white;
}
&-label {
font-weight: 500;
color: #00695C;
padding: 0 8px;
border-radius: 4px;
border: 1px solid rgba(#00695C, 0.32);
background-color: rgba(#00695C, 0.04);
}
}
}

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

@ -0,0 +1,239 @@
///
/// 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.
///
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Observable, of, Subject } from 'rxjs';
import {
BooleanOperation,
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
entityKeyValueTypeToFilterPredicateType,
FilterPredicateType,
NumericOperation,
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 {
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleComplexFilterPredicateDialogData
} from "@home/components/alarm-rules/filter/alarm-rule-complex-filter-predicate-dialog.component";
import {
AlarmRuleFilterPredicate,
AlarmRulePredicateInfo,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
@Component({
selector: 'tb-alarm-rule-filter-predicate-list',
templateUrl: './alarm-rule-filter-predicate-list.component.html',
styleUrls: ['./alarm-rule-filter-predicate-list.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmRuleFilterPredicateListComponent),
multi: true
}
]
})
export class AlarmRuleFilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@Input() disabled: boolean;
@Input() valueType: EntityKeyValueType;
@Input() operation: ComplexOperation = ComplexOperation.AND;
@Input() arguments: Record<string, CalculatedFieldArgument>;
filterListFormGroup: UntypedFormGroup;
valueTypeEnum = EntityKeyValueType;
complexOperationTranslations = complexOperationTranslationMap;
private destroy$ = new Subject<void>();
private propagateChange = null;
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([])
});
this.filterListFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => this.updateModel());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
get predicatesFormArray(): UntypedFormArray {
return this.filterListFormGroup.get('predicates') as UntypedFormArray;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.filterListFormGroup.disable({emitEvent: false});
} else {
this.filterListFormGroup.enable({emitEvent: false});
}
}
validate(control: AbstractControl): ValidationErrors | null {
return this.filterListFormGroup.valid ? null : {
filterList: {valid: false}
};
}
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});
}
}
}
public removePredicate(index: number) {
this.predicatesFormArray.removeAt(index);
}
public addPredicate(complex: boolean) {
const predicatesFormArray = this.filterListFormGroup.get('predicates') as UntypedFormArray;
const predicate = this.createDefaultFilterPredicate(this.valueType, complex);
let observable: Observable<AlarmRuleFilterPredicate>;
if (complex) {
observable = this.openComplexFilterDialog(predicate as ComplexAlarmRuleFilterPredicate);
} else {
observable = of(predicate);
}
observable.subscribe((result) => {
if (result) {
predicatesFormArray.push(this.fb.control(result, [Validators.required]));
}
});
}
private createDefaultFilterPredicate(valueType: EntityKeyValueType, complex: boolean): AlarmRuleFilterPredicate {
const predicate = {
type: complex ? FilterPredicateType.COMPLEX : entityKeyValueTypeToFilterPredicateType(valueType)
} as AlarmRuleFilterPredicate;
switch (predicate.type) {
case FilterPredicateType.STRING:
predicate.operation = StringOperation.STARTS_WITH;
predicate.value = {
staticValue: ''
};
predicate.ignoreCase = false;
break;
case FilterPredicateType.NUMERIC:
predicate.operation = NumericOperation.EQUAL;
predicate.value = {
staticValue: valueType === EntityKeyValueType.DATE_TIME ? Date.now() : 0
};
break;
case FilterPredicateType.BOOLEAN:
predicate.operation = BooleanOperation.EQUAL;
predicate.value = {
staticValue: false
};
break;
case FilterPredicateType.COMPLEX:
predicate.operation = ComplexOperation.AND;
predicate.predicates = [];
break;
}
return predicate;
}
private openComplexFilterDialog(predicate: ComplexAlarmRuleFilterPredicate): Observable<ComplexAlarmRuleFilterPredicate> {
return this.dialog.open<AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogData,
ComplexAlarmRuleFilterPredicate>(AlarmRuleComplexFilterPredicateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: predicate as ComplexAlarmRuleFilterPredicate,
valueType: this.valueType,
isAdd: true,
arguments: this.arguments,
}
}).afterClosed().pipe(
map((result) => {
if (result) {
predicate = result;
return predicate;
} else {
return null;
}
})
);
}
private updateModel() {
const predicates: Array<AlarmRulePredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
if (predicates.length) {
this.propagateChange(predicates);
} else {
this.propagateChange(null);
}
}
}

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

@ -0,0 +1,80 @@
<!--
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="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>
</mat-form-field>
@if (mode === 'static') {
@switch (valueType) {
@case (valueTypeEnum.STRING) {
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="staticValue" placeholder="{{'filter.value' | translate}}">
</mat-form-field>
}
@case (valueTypeEnum.NUMERIC) {
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input required type="number" matInput formControlName="staticValue"
placeholder="{{'filter.value' | translate}}">
</mat-form-field>
}
@case (valueTypeEnum.BOOLEAN) {
<mat-checkbox formControlName="staticValue">
{{ (filterPredicateValueFormGroup.get('staticValue').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
}
@case (valueTypeEnum.DATE_TIME) {
<tb-datetime formControlName="staticValue"
class="flex-1"
appearance="outline"
subscriptSizing="dynamic"
fieldClass="flex-1"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
}
}
} @else {
<mat-form-field class="flex-1 w-full" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="dynamicValueArgument" placeholder="{{ 'action.set' | translate }}">
@for (argument of argumentsList; track argument) {
<mat-option [value]="argument">{{ argument }}</mat-option>
}
</mat-select>
@if (filterPredicateValueFormGroup.get('dynamicValueArgument').touched && filterPredicateValueFormGroup.get('dynamicValueArgument').hasError('required')) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.argument-name-required' | translate"
class="tb-error !block">
warning
</mat-icon>
}
</mat-form-field>
}
</div>
</div>

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

@ -0,0 +1,159 @@
///
/// 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.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
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({
selector: 'tb-alarm-rule-filter-predicate-value',
templateUrl: './alarm-rule-filter-predicate-value.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmRuleFilterPredicateValueComponent),
multi: true
}
]
})
export class AlarmRuleFilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
valueType: EntityKeyValueType;
valueTypeEnum = EntityKeyValueType;
filterPredicateValueFormGroup: FormGroup<FormControlsFrom<AlarmRuleValue<string | number | boolean>>>;
mode: 'static' | 'dynamic' = 'static';
private propagateChange = null;
private propagateChangePending = false;
constructor(private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
let defaultValue: string | number | boolean;
let defaultValueValidators: ValidatorFn[];
switch (this.valueType) {
case EntityKeyValueType.STRING:
defaultValue = '';
defaultValueValidators = [];
break;
case EntityKeyValueType.NUMERIC:
defaultValue = 0;
defaultValueValidators = [Validators.required];
break;
case EntityKeyValueType.BOOLEAN:
defaultValue = false;
defaultValueValidators = [];
break;
case EntityKeyValueType.DATE_TIME:
defaultValue = Date.now();
defaultValueValidators = [Validators.required];
break;
}
this.filterPredicateValueFormGroup = this.fb.group({
staticValue: [defaultValue, defaultValueValidators],
dynamicValueArgument: [null, Validators.required]
});
this.filterPredicateValueFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateModel();
});
}
private updateValueModeValidators(mode: 'static' | 'dynamic'): void {
if (mode === 'static') {
this.filterPredicateValueFormGroup.get('staticValue').enable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').disable({emitEvent: false});
} else {
this.filterPredicateValueFormGroup.get('staticValue').disable({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValueArgument').enable({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 {
}
validate(): ValidationErrors | null {
return this.filterPredicateValueFormGroup.valid ? null : {
filterPredicateValue: {valid: false}
};
}
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);
}
private updateModel() {
const predicateValue: AlarmRuleValue<string | number | boolean> = this.filterPredicateValueFormGroup.value;
if (this.propagateChange) {
this.propagateChange(predicateValue);
} else {
this.propagateChangePending = true;
}
}
}

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

@ -0,0 +1,79 @@
<!--
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-row items-center justify-start gap-4" [formGroup]="filterPredicateFormGroup">
<div class="tb-form-panel no-border no-padding-right flex-1" style="flex-direction: row">
@switch (type) {
@case (filterPredicateType.STRING) {
<div class="tb-form-row no-padding no-border column-xs w-full max-w-30% xs:max-w-full flex-1/">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation">
{{stringOperationTranslationMap.get(stringOperation[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<mat-chip-listbox formControlName="ignoreCase" [hideSingleSelectionIndicator]="true">
<mat-chip-option color="primary" [value]="true">{{ 'alarm-rule.ignore-case' | translate }}</mat-chip-option>
</mat-chip-listbox>
</div>
}
@case (filterPredicateType.NUMERIC) {
<div class="tb-form-row no-border no-padding no-border w-full max-w-30%">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation">
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
}
@case (filterPredicateType.BOOLEAN) {
<div class="tb-form-row no-border no-padding no-border w-full max-w-30%">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
}
@case (filterPredicateType.COMPLEX) {
<div class="tb-form-row no-border no-padding flex-1">
<button type="button" style="--mat-outlined-button-horizontal-padding: 3px 0px 12px;"
class="block w-full"
mat-stroked-button color="primary"
(click)="openComplexFilterDialog()">
<div class="flex items-center gap-2">
<span class="w-full text-start" translate>filter.complex-filter</span>
<mat-icon class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">edit</mat-icon>
</div>
</button>
</div>
}
}
@if (type !== filterPredicateType.COMPLEX) {
<tb-alarm-rule-filter-predicate-value class="flex-full"
[arguments]="arguments"
[valueType]="valueType"
formControlName="value">
</tb-alarm-rule-filter-predicate-value>
}
</div>
</div>

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

@ -0,0 +1,158 @@
///
/// 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.
///
import { Component, DestroyRef, forwardRef, Input, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
ValidationErrors,
Validator
} from '@angular/forms';
import {
BooleanOperation,
booleanOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
numericOperationTranslationMap,
StringOperation,
stringOperationTranslationMap
} from '@shared/models/query/query.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AlarmRuleFilterPredicate, ComplexAlarmRuleFilterPredicate } from "@shared/models/alarm-rule.models";
import { MatDialog } from "@angular/material/dialog";
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({
selector: 'tb-alarm-rule-filter-predicate',
templateUrl: './alarm-rule-filter-predicate.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AlarmRuleFilterPredicateComponent),
multi: true
}
]
})
export class AlarmRuleFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input()
valueType: EntityKeyValueType;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
filterPredicateFormGroup: FormGroup<FormControlsFrom<AlarmRuleFilterPredicate>>;
type: FilterPredicateType;
filterPredicateType = FilterPredicateType;
stringOperations = Object.keys(StringOperation);
stringOperation = StringOperation;
stringOperationTranslationMap = stringOperationTranslationMap;
numericOperations = Object.keys(NumericOperation);
numericOperationEnum = NumericOperation;
numericOperationTranslations = numericOperationTranslationMap;
booleanOperations = Object.keys(BooleanOperation);
booleanOperationEnum = BooleanOperation;
booleanOperationTranslations = booleanOperationTranslationMap;
private propagateChange = null;
constructor(private fb: UntypedFormBuilder,
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(() => {
this.updateModel();
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
validate(): ValidationErrors | null {
return this.filterPredicateFormGroup.valid ? null : {
filterPredicate: {valid: false}
};
}
writeValue(predicate: AlarmRuleFilterPredicate): void {
this.type = predicate.type;
this.filterPredicateFormGroup.patchValue(predicate, {emitEvent: false});
}
private updateModel() {
let predicate: AlarmRuleFilterPredicate = null;
if (this.filterPredicateFormGroup.valid) {
predicate = this.filterPredicateFormGroup.getRawValue();
predicate.type = this.type;
}
this.propagateChange(predicate);
}
public openComplexFilterDialog() {
this.dialog.open<AlarmRuleComplexFilterPredicateDialogComponent, AlarmRuleComplexFilterPredicateDialogData,
ComplexAlarmRuleFilterPredicate>(AlarmRuleComplexFilterPredicateDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
complexPredicate: this.filterPredicateFormGroup.getRawValue() as ComplexAlarmRuleFilterPredicate,
valueType: this.valueType,
isAdd: false,
arguments: this.arguments,
}
}).afterClosed().subscribe(
(result) => {
if (result) {
this.filterPredicateFormGroup.patchValue(result, {emitEvent: false});
this.updateModel();
}
}
);
}
}

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

@ -0,0 +1,23 @@
<!--
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="tb-filter-text"
tbTruncateWithTooltip
[class.required]="requiredClass"
[class.nowrap]="nowrap"
[innerHTML]="filterText">
</div>

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

@ -0,0 +1,85 @@
/**
* 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.
*/
:host {
text-overflow: ellipsis;
overflow: hidden;
.tb-filter-text {
overflow-y: auto;
text-align: start;
&.required {
color: #f44336;
padding: 0;
}
&.nowrap {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
:host ::ng-deep {
.tb-filter-text {
line-height: 1.8em;
span {
display: inline-block;
vertical-align: middle;
line-height: 1.4em;
}
.tb-filter-predicate {
padding-right: 4px;
padding-left: 4px;
}
.tb-filter-entity-key, .tb-filter-value, .tb-filter-dynamic-source {
font-weight: bold;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
padding-left: 4px;
padding-right: 4px;
}
.tb-filter-entity-key, .tb-filter-value {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.tb-filter-dynamic-source {
}
.tb-filter-entity-key {
color: #305680;
}
.tb-filter-value {
color: #ff5722;
}
.tb-filter-simple-operation {
font-size: 0.9em;
}
.tb-filter-complex-operation {
font-weight: 400;
font-style: italic;
}
.tb-filter-dynamic-value {
.tb-filter-dynamic-source, .tb-filter-value {
color: #0c959c;
}
}
.tb-filter-bracket {
.tb-left-bracket, .tb-right-bracket {
font-size: 1.2em;
}
}
}
}

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

@ -0,0 +1,192 @@
///
/// 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.
///
import { Component, Input } from '@angular/core';
import {
booleanOperationTranslationMap,
ComplexOperation,
complexOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType,
numericOperationTranslationMap,
stringOperationTranslationMap
} from '@shared/models/query/query.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import {
AlarmRuleExpression,
AlarmRuleExpressionType,
AlarmRuleFilter,
AlarmRuleFilterPredicate,
ComplexAlarmRuleFilterPredicate
} from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import { coerceBoolean } from "@shared/decorators/coercion";
@Component({
selector: 'tb-alarm-rule-filter-text',
templateUrl: './alarm-rule-filter-text.component.html',
styleUrls: ['./alarm-rule-filter-text.component.scss'],
providers: []
})
export class AlarmRuleFilterTextComponent {
@Input()
@coerceBoolean()
required = false;
@Input()
noFilterText = this.translate.instant('filter.no-filter-text');
@Input()
addFilterPrompt = this.translate.instant('filter.add-filter-prompt');
@Input()
@coerceBoolean()
nowrap = false;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
private alarmRuleExpressionValue: AlarmRuleExpression;
get alarmRuleExpression(): AlarmRuleExpression {
return this.alarmRuleExpressionValue;
}
@Input()
set alarmRuleExpression(value: AlarmRuleExpression) {
if (value !== this.alarmRuleExpressionValue) {
this.alarmRuleExpressionValue = value;
this.updateFilterText(value);
}
};
private specTextValue: string;
get specText(): string {
return this.specTextValue;
}
@Input()
set specText(value: string) {
if (value !== this.specTextValue) {
this.specTextValue = value;
this.updateFilterText(this.alarmRuleExpression);
}
}
requiredClass = false;
public filterText: string;
constructor(private translate: TranslateService,
private datePipe: DatePipe) {
}
private updateFilterText(value: AlarmRuleExpression) {
this.requiredClass = false;
if (value && (value.expression || value.filters)) {
if (value.type === AlarmRuleExpressionType.SIMPLE) {
this.filterText = this.keyFiltersToText(this.translate, this.datePipe, value.filters, value.operation);
} else {
this.filterText = 'function expression(ctx, ' + (this.arguments ? Object.keys(this.arguments).join(', ') : '' ) + ')';
}
if (this.specText?.length) {
this.filterText = this.specText + ': ' + this.filterText;
}
} else {
if (this.required) {
this.filterText = this.addFilterPrompt;
this.requiredClass = true;
} else {
this.filterText = this.noFilterText;
}
}
}
private keyFiltersToText(translate: TranslateService, datePipe: DatePipe, keyFilters: Array<AlarmRuleFilter>, operation: ComplexOperation): string {
const filtersText = keyFilters.map(keyFilter =>
this.filterPredicateToText(translate, datePipe, keyFilter, keyFilter.predicates));
let result: string;
if (filtersText.length > 1) {
const operationText = translate.instant(complexOperationTranslationMap.get(operation));
result = filtersText.join(' <span class="tb-filter-complex-operation">' + operationText + '</span> ');
} else {
result = filtersText[0];
}
return result;
}
private filterPredicateToText(translate: TranslateService,
datePipe: DatePipe,
keyFilter: AlarmRuleFilter,
keyFilterPredicates: AlarmRuleFilterPredicate[],
complexOperation?: ComplexOperation): string {
const key = keyFilter.argument;
const filterOperation: ComplexOperation = complexOperation ? complexOperation : (keyFilter.operation ?? ComplexOperation.AND);
const predicates = keyFilterPredicates.map((keyFilterPredicate: AlarmRuleFilterPredicate) => {
if (keyFilterPredicate.type === FilterPredicateType.COMPLEX) {
const complexPredicate = keyFilterPredicate as ComplexAlarmRuleFilterPredicate;
const complexOperation = complexPredicate.operation ?? ComplexOperation.AND;
return this.filterPredicateToText(translate, datePipe, keyFilter, complexPredicate.predicates, complexOperation);
} else {
let operation: string;
let value: string;
const val = keyFilterPredicate.value;
const dynamicValue = val?.dynamicValueArgument?.length;
if (dynamicValue) {
value = '<span class="tb-filter-dynamic-value"><span class="tb-filter-value">' + val?.dynamicValueArgument + '</span></span>';
}
switch (keyFilterPredicate.type) {
case FilterPredicateType.STRING:
operation = translate.instant(stringOperationTranslationMap.get(keyFilterPredicate.operation));
if (keyFilterPredicate.ignoreCase) {
operation += ' ' + translate.instant('filter.ignore-case');
}
if (!dynamicValue) {
value = `'${keyFilterPredicate.value.staticValue}'`;
}
break;
case FilterPredicateType.NUMERIC:
operation = translate.instant(numericOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) {
if (keyFilter.valueType === EntityKeyValueType.DATE_TIME) {
value = datePipe.transform(keyFilterPredicate.value.staticValue, 'yyyy-MM-dd HH:mm');
} else {
value = keyFilterPredicate.value.staticValue + '';
}
}
break;
case FilterPredicateType.BOOLEAN:
operation = translate.instant(booleanOperationTranslationMap.get(keyFilterPredicate.operation));
if (!dynamicValue) {
value = translate.instant(keyFilterPredicate.value.staticValue ? 'value.true' : 'value.false');
}
break;
}
if (!dynamicValue) {
value = `<span class="tb-filter-value">${value}</span>`;
}
return `<span class="tb-filter-predicate"><span class="tb-filter-entity-key">${key}</span> <span class="tb-filter-simple-operation">${operation}</span> ${value}</span>`
}
});
if (predicates.length > 1) {
return '(' + predicates.join(` ${translate.instant(complexOperationTranslationMap.get(filterOperation))} `)+ ')';
} else {
return predicates.toString();
}
}
}

2
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -77,6 +77,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
private destroyRef: DestroyRef,
private renderer: Renderer2,
public entityName: string,
private ownerId: EntityId = null,
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
) {
@ -216,6 +217,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
entityId: this.entityId,
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
additionalDebugActionConfig: this.additionalDebugActionConfig,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
isDirty,

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

@ -50,6 +50,7 @@ export class CalculatedFieldsTableComponent {
active = input<boolean>();
entityId = input<EntityId>();
entityName = input<string>();
ownerId = input<EntityId>();
calculatedFieldsTableConfig: CalculatedFieldsTableConfig;
@ -76,6 +77,7 @@ export class CalculatedFieldsTableComponent {
this.destroyRef,
this.renderer,
this.entityName(),
this.ownerId(),
this.importExportService,
this.entityDebugSettingsService,
);

13
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,
CalculatedFieldArgumentValue, CFArgumentDynamicSourceType,
getCalculatedFieldCurrentEntityFilter
} from '@shared/models/calculated-field.models';
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
@ -65,6 +65,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() entityId: EntityId;
@Input() tenantId: string;
@Input() entityName: string;
@Input() ownerId: EntityId;
@Input() isScript: boolean;
@Input() usedArgumentNames: string[];
@Input() isOutputKey = false;
@ -166,7 +167,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
saveArgument(): void {
const value = this.argumentFormGroup.value as CalculatedFieldArgumentValue;
if (this.entityType === ArgumentEntityType.Tenant) {
if (this.entityType === ArgumentEntityType.Owner) {
value.refDynamicSource = CFArgumentDynamicSourceType.CURRENT_OWNER;
} else if (this.entityType === ArgumentEntityType.Tenant) {
value.refEntityId = new TenantId(this.tenantId) as any;
}
if (this.entityType !== ArgumentEntityType.Current && this.entityType !== ArgumentEntityType.Tenant) {
@ -206,6 +209,12 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
case ArgumentEntityType.Current:
entityFilter = this.currentEntityFilter;
break;
case ArgumentEntityType.Owner:
entityFilter = {
type: AliasFilterType.singleEntity,
singleEntity: this.ownerId
};
break;
case ArgumentEntityType.Tenant:
entityFilter = {
type: AliasFilterType.singleEntity,

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

@ -85,6 +85,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() entityId: EntityId;
@Input() tenantId: string;
@Input() entityName: string;
@Input() ownerId: EntityId;
@Input() isScript: boolean;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -179,6 +180,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
buttonTitle: isExists ? 'action.apply' : 'action.add',
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
};
this.popoverComponent = this.popoverService.displayPopover({

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

@ -89,6 +89,7 @@
<tb-simple-configuration formControlName="configuration"
[entityId]="data.entityId"
[entityName]="data.entityName"
[ownerId]="data.ownerId"
[tenantId]="data.tenantId"
[isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)"

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

@ -44,6 +44,7 @@ export interface CalculatedFieldDialogData {
entityId: EntityId;
tenantId: string;
entityName?: string;
ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
getTestScriptDialogFn: CalculatedFieldTestScriptFn;
isDirty?: boolean;
@ -71,7 +72,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
readonly EntityType = EntityType;
readonly CalculatedFieldType = CalculatedFieldType;
readonly fieldTypes = Object.values(CalculatedFieldType) as CalculatedFieldType[];
readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[];
readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations;
constructor(protected store: Store<AppState>,

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

@ -21,6 +21,7 @@
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="entityId"
[tenantId]="tenantId"
[ownerId]="ownerId"
[entityName]="entityName"
[isScript]="isScript" />
</div>

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

@ -75,6 +75,9 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
@Input({required: true})
entityName: string;
@Input({required: true})
ownerId: EntityId;
@Input({required: true})
testScript: () => Observable<string>;

5
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -195,6 +195,8 @@ import {
CalculatedFieldDebugDialogComponent
} from '@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component';
import { CalculatedFieldsModule } from '@home/components/calculated-fields/calculated-field.module';
import { AlarmRuleModule } from "@home/components/alarm-rules/alarm-rule.module";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
@NgModule({
declarations:
@ -209,6 +211,7 @@ import { CalculatedFieldsModule } from '@home/components/calculated-fields/calcu
AuditLogDetailsDialogComponent,
CalculatedFieldsTableComponent,
CalculatedFieldDebugDialogComponent,
AlarmRulesTableComponent,
EventContentDialogComponent,
EventTableHeaderComponent,
EventTableComponent,
@ -351,6 +354,7 @@ import { CalculatedFieldsModule } from '@home/components/calculated-fields/calcu
SharedModule,
SharedHomeComponentsModule,
CalculatedFieldsModule,
AlarmRuleModule,
WidgetConfigComponentsModule,
BasicWidgetConfigModule,
Lwm2mProfileComponentsModule,
@ -369,6 +373,7 @@ import { CalculatedFieldsModule } from '@home/components/calculated-fields/calcu
EntityDetailsPageComponent,
AuditLogTableComponent,
CalculatedFieldsTableComponent,
AlarmRulesTableComponent,
EventTableComponent,
EdgeDownlinkTableHeaderComponent,
EdgeDownlinkTableComponent,

40
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html

@ -15,17 +15,29 @@
limitations under the License.
-->
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [entityName]="entity.name"/>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab"
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>
@if (entity && !isEdit) {
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"/>
</mat-tab>
}
<mat-tab #alarmRules="matTab" label="{{'alarm-rule.alarm-rules' | translate }}">
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table>
</mat-tab>
<mat-tab #auditLogsTab="matTab"
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true"
singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name"
[externalEntityId]="entity.externalId || entity.id">
</tb-version-control>
</mat-tab>
}
}

93
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html

@ -15,47 +15,52 @@
limitations under the License.
-->
<mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id"/>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'asset.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>
@if (entity) {
<mat-tab label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="entity.tenantId"/>
</mat-tab>
<mat-tab #alarmRules="matTab"
label="{{'alarm-rule.alarm-rules' | translate }}">
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name"></tb-alarm-rules-table>
</mat-tab>
}
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab label="{{ 'asset.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id"
[entityName]="entity.name"
[externalEntityId]="entity.externalId || entity.id">
</tb-version-control>
</mat-tab>
}
}

94
ui-ngx/src/app/modules/home/pages/customer/customer-tabs.component.html

@ -15,43 +15,57 @@
limitations under the License.
-->
<mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'customer.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.CUSTOMER" [customerId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>
@if (entity) {
<mat-tab label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.SERVER_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab #alarmRulesTab="matTab" label="{{'alarm-rule.alarm-rules-tab' | translate }}">
<tb-alarm-rules-table [active]="alarmRulesTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table>
</mat-tab>
}
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab label="{{ 'customer.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR"
[active]="eventsTab.isActive"
[tenantId]="entity.tenantId.id"
[entityId]="entity.id">
</tb-event-table>
</mat-tab>
<mat-tab label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true"
[active]="auditLogsTab.isActive"
[auditLogMode]="auditLogModes.CUSTOMER"
[customerId]="entity.id">
</tb-audit-log-table>
</mat-tab>
<mat-tab label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true"
singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name"
[externalEntityId]="entity.externalId || entity.id">
</tb-version-control>
</mat-tab>
}
}

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

@ -15,54 +15,88 @@
limitations under the License.
-->
<mat-tab *ngIf="entity" #transportType="matTab"
label="{{ 'device-profile.transport-configuration' | translate }}">
<div class="mat-padding" [formGroup]="detailsForm" *ngIf="transportType.isActive">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="detailsForm.get('transportType').value">
{{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="detailsForm.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<div formGroupName="profileData">
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
[isAdd]="isTransportTypeChanged">
</tb-device-profile-transport-configuration>
</div>
</div>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [entityName]="entity.name"/>
</mat-tab>
<mat-tab *ngIf="entity" #alarmRules="matTab"
label="{{ this.detailsForm.get('profileData.alarms').value?.length
? ('device-profile.alarm-rules-with-count' | translate: { count: this.detailsForm.get('profileData.alarms').value.length })
: 'device-profile.alarm-rules' | translate }}">
<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>
@if (entity) {
<mat-tab #transportType="matTab" label="{{ 'device-profile.transport-configuration' | translate }}">
<div class="mat-padding" [formGroup]="detailsForm" *ngIf="transportType.isActive">
<mat-form-field class="mat-block">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="detailsForm.get('transportType').value">
{{deviceTransportTypeHints.get(detailsForm.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="detailsForm.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<div formGroupName="profileData">
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
[isAdd]="isTransportTypeChanged">
</tb-device-profile-transport-configuration>
</div>
</div>
</div>
</mat-tab>
<mat-tab *ngIf="entity" #deviceProvisioning="matTab"
label="{{ 'device-profile.device-provisioning' | translate }}">
<div class="mat-padding" [formGroup]="detailsForm" *ngIf="deviceProvisioning.isActive">
<div formGroupName="profileData">
<tb-device-profile-provision-configuration formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"/>
</mat-tab>
}
@if (hasOldRules || authUser.authority === authorities.TENANT_ADMIN && !isEdit) {
<mat-tab #alarmRules="matTab" label="{{'alarm-rule.alarm-rules-tab' | translate }}">
<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>
</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 {
<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>
}
</section>
</mat-tab>
}
<mat-tab #deviceProvisioning="matTab" label="{{ 'device-profile.device-provisioning' | translate }}">
<div class="mat-padding" [formGroup]="detailsForm" *ngIf="deviceProvisioning.isActive">
<div formGroupName="profileData">
<tb-device-profile-provision-configuration formControlName="provisionConfiguration">
</tb-device-profile-provision-configuration>
</div>
</div>
</div>
</mat-tab>
</mat-tab>
@if (!isEdit) {
<mat-tab #auditLogsTab="matTab" label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
}
@if (authUser.authority === authorities.TENANT_ADMIN && !isEdit) {
<mat-tab label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name"
[externalEntityId]="entity.externalId || entity.id">
</tb-version-control>
</mat-tab>
}
}
<mat-tab *ngIf="false"
label="{{'device-profile.profile-configuration' | translate }}" #deviceProfile="matTab">
<div class="mat-padding" [formGroup]="detailsForm">
@ -73,13 +107,3 @@
</div>
</div>
</mat-tab>
<mat-tab *ngIf="entity && !isEdit" #auditLogsTab="matTab"
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

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

@ -41,6 +41,9 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
isTransportTypeChanged = false;
hasOldRules = false;
alarmRulesVersion = false;
constructor(protected store: Store<AppState>,
private destroyRef: DestroyRef) {
super(store);
@ -57,6 +60,8 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
protected setEntity(entity: DeviceProfile) {
this.isTransportTypeChanged = false;
this.hasOldRules = !!entity?.profileData?.alarms?.length;
this.alarmRulesVersion = !this.hasOldRules;
super.setEntity(entity);
}

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

@ -15,47 +15,48 @@
limitations under the License.
-->
<mat-tab *ngIf="entity"
label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.CLIENT_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id"/>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'device.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab *ngIf="entity"
label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>
@if (entity) {
<mat-tab label="{{ 'attribute.attributes' | translate }}" #attributesTab="matTab">
<tb-attribute-table [defaultAttributeScope]="attributeScopes.CLIENT_SCOPE"
[active]="attributesTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
<mat-tab label="{{ 'attribute.latest-telemetry' | translate }}" #telemetryTab="matTab">
<tb-attribute-table [defaultAttributeScope]="latestTelemetryTypes.LATEST_TELEMETRY"
disableAttributeScopeSelection
[active]="telemetryTab.isActive"
[entityId]="entity.id"
[entityName]="entity.name">
</tb-attribute-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id"/>
</mat-tab>
<mat-tab #calculatedFieldsAlarmRules="matTab" label="{{'alarm-rule.alarm-rules-tab' | translate }}">
<tb-alarm-rules-table [active]="calculatedFieldsAlarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name"></tb-alarm-rules-table>
</mat-tab>
}
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">
<tb-alarm-table [active]="alarmsTab.isActive" [entityId]="entity.id"></tb-alarm-table>
</mat-tab>
<mat-tab label="{{ 'device.events' | translate }}" #eventsTab="matTab">
<tb-event-table [defaultEventType]="eventTypes.ERROR" [active]="eventsTab.isActive" [tenantId]="entity.tenantId.id"
[entityId]="entity.id"></tb-event-table>
</mat-tab>
<mat-tab label="{{ 'relation.relations' | translate }}" #relationsTab="matTab">
<tb-relation-table [active]="relationsTab.isActive" [entityId]="entity.id"></tb-relation-table>
</mat-tab>
<mat-tab label="{{ 'audit-log.audit-logs' | translate }}" #auditLogsTab="matTab">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>
}
}

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

@ -0,0 +1,152 @@
///
/// 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.
///
import { CustomTimeSchedulerItem } from "@shared/models/device.models";
import { DashboardId } from "@shared/models/id/dashboard-id";
import { TimeUnit } from "@shared/models/time/time.models";
import {
BooleanOperation,
ComplexOperation,
EntityKeyValueType,
FilterPredicateType,
NumericOperation,
StringOperation
} from "@shared/models/query/query.models";
export enum AlarmRuleScheduleType {
ANY_TIME = 'ANY_TIME',
SPECIFIC_TIME = 'SPECIFIC_TIME',
CUSTOM = 'CUSTOM'
}
export const AlarmRuleScheduleTypeTranslationMap = new Map<AlarmRuleScheduleType, string>(
[
[AlarmRuleScheduleType.ANY_TIME, 'alarm-rule.schedule.any-time'],
[AlarmRuleScheduleType.SPECIFIC_TIME, 'alarm-rule.schedule.specific-time'],
[AlarmRuleScheduleType.CUSTOM, 'alarm-rule.schedule.custom']
]
);
export enum AlarmRuleConditionType {
SIMPLE = 'SIMPLE',
DURATION = 'DURATION',
REPEATING = 'REPEATING'
}
export const AlarmRuleConditionTypeTranslationMap = new Map<AlarmRuleConditionType, string>(
[
[AlarmRuleConditionType.SIMPLE, 'alarm-rule.conditions.simple'],
[AlarmRuleConditionType.DURATION, 'alarm-rule.conditions.duration'],
[AlarmRuleConditionType.REPEATING, 'alarm-rule.conditions.repeating']
]
);
export enum AlarmRuleExpressionType {
SIMPLE = 'SIMPLE',
TBEL = 'TBEL',
}
export const FilterPredicateTypeTranslationMap = new Map<FilterPredicateType, string>(
[
[FilterPredicateType.STRING, 'alarm-rule.filter-predicate-type.string'],
[FilterPredicateType.NUMERIC, 'alarm-rule.filter-predicate-type.numeric'],
[FilterPredicateType.BOOLEAN, 'alarm-rule.filter-predicate-type.boolean'],
[FilterPredicateType.COMPLEX, 'alarm-rule.filter-predicate-type.complex']
]
);
export interface AlarmRule {
condition: AlarmRuleCondition;
alarmDetails?: string;
dashboardId?: DashboardId;
}
export interface AlarmRuleCondition {
type: AlarmRuleConditionType;
expression: AlarmRuleExpression;
schedule?: AlarmRuleSchedule;
unit?: TimeUnit;
value?: AlarmRuleValue<number>;
count?: AlarmRuleValue<number>;
}
export interface AlarmRuleExpression {
type: AlarmRuleExpressionType;
expression?: string;
filters?: Array<AlarmRuleFilter>;
operation?: ComplexOperation;
}
export interface AlarmRuleSchedule {
staticValue?: {
type?: AlarmRuleScheduleType;
timezone?: string;
daysOfWeek?: number[];
startsOn?: number;
endsOn?: number;
items?: CustomTimeSchedulerItem[];
};
dynamicValueArgument?: string;
}
export interface AlarmRuleFilter {
argument: string;
valueType: EntityKeyValueType;
operation: ComplexOperation;
predicates: AlarmRuleFilterPredicate[];
}
export interface AlarmRulePredicateInfo {
keyFilterPredicate: AlarmRuleFilterPredicate;
}
export type AlarmRuleFilterPredicate = StringAlarmRuleFilterPredicate |
NumericAlarmRuleFilterPredicate |
BooleanAlarmRuleFilterPredicate |
ComplexAlarmRuleFilterPredicate;
export interface AlarmRuleValue<T> {
dynamicValueArgument?: string;
staticValue?: T
}
export interface StringAlarmRuleFilterPredicate {
type: FilterPredicateType.STRING;
operation: StringOperation;
value: AlarmRuleValue<string>;
ignoreCase: boolean;
}
export interface NumericAlarmRuleFilterPredicate {
type: FilterPredicateType.NUMERIC;
operation: NumericOperation;
value: AlarmRuleValue<number>;
}
export interface BooleanAlarmRuleFilterPredicate {
type: FilterPredicateType.BOOLEAN;
operation: BooleanOperation;
value: AlarmRuleValue<boolean>;
}
export interface BaseComplexFilterPredicate<T extends AlarmRuleFilterPredicate> {
type: FilterPredicateType.COMPLEX;
operation: ComplexOperation;
predicates: Array<T>;
}
export type ComplexAlarmRuleFilterPredicate = BaseComplexFilterPredicate<AlarmRuleFilterPredicate>;

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

@ -31,6 +31,7 @@ import {
} from '@shared/models/ace/ace.models';
import { EntitySearchDirection } from '@shared/models/relation.models';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import { AlarmRule } from "@shared/models/alarm-rule.models";
interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
entityId: EntityId;
@ -56,18 +57,25 @@ export interface CalculatedFieldPropagation extends BaseCalculatedField {
configuration: CalculatedFieldPropagationConfiguration;
}
export interface CalculatedFieldAlarmRule extends BaseCalculatedField {
type: CalculatedFieldType.ALARM;
configuration: CalculatedFieldAlarmRuleConfiguration;
}
export type CalculatedField =
| CalculatedFieldSimple
| CalculatedFieldScript
| CalculatedFieldGeofencing
| CalculatedFieldPropagation;
| CalculatedFieldPropagation
| CalculatedFieldAlarmRule;
export enum CalculatedFieldType {
SIMPLE = 'SIMPLE',
SCRIPT = 'SCRIPT',
GEOFENCING = 'GEOFENCING',
PROPAGATION = 'PROPAGATION',
RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION'
RELATED_ENTITIES_AGGREGATION = 'RELATED_ENTITIES_AGGREGATION',
ALARM = 'ALARM',
}
export const CalculatedFieldTypeTranslations = new Map<CalculatedFieldType, string>(
@ -85,7 +93,8 @@ export type CalculatedFieldConfiguration =
| CalculatedFieldScriptConfiguration
| CalculatedFieldGeofencingConfiguration
| CalculatedFieldPropagationConfiguration
| CalculatedFieldRelatedAggregationConfiguration;
| CalculatedFieldRelatedAggregationConfiguration
| CalculatedFieldAlarmRuleConfiguration;
export interface CalculatedFieldSimpleConfiguration {
type: CalculatedFieldType.SIMPLE;
@ -127,6 +136,17 @@ interface BasePropagationConfiguration {
output: CalculatedFieldOutput;
}
interface CalculatedFieldAlarmRuleConfiguration {
type: CalculatedFieldType.ALARM;
arguments: Record<string, CalculatedFieldArgument>;
createRules: {[severity: string]: AlarmRule};
clearRule?: AlarmRule;
propagate?: boolean;
propagateToOwner?: boolean;
propagateToTenant?: boolean;
propagateRelationTypes?: Array<string>;
}
export interface PropagationWithNoExpression extends BasePropagationConfiguration {
applyExpressionToResolvedArguments: false;
}
@ -156,6 +176,7 @@ export enum ArgumentEntityType {
Asset = 'ASSET',
Customer = 'CUSTOMER',
Tenant = 'TENANT',
Owner = 'CURRENT_OWNER',
RelationQuery = 'RELATION_PATH_QUERY',
}
@ -166,6 +187,7 @@ export const ArgumentEntityTypeTranslations = new Map<ArgumentEntityType, string
[ArgumentEntityType.Asset, 'calculated-fields.argument-asset'],
[ArgumentEntityType.Customer, 'calculated-fields.argument-customer'],
[ArgumentEntityType.Tenant, 'calculated-fields.argument-tenant'],
[ArgumentEntityType.Owner, 'calculated-fields.argument-owner'],
[ArgumentEntityType.RelationQuery, 'calculated-fields.argument-relation-query'],
]
)
@ -244,10 +266,15 @@ 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;
limit?: number;
timeWindow?: number;
}

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

@ -0,0 +1,123 @@
#### Active all time schedule format
An attribute with a dynamic value for an active all-time schedule format must contain an empty JSON object or JSON in the following format:
```javascript
{
"type": "ANY_TIME"
}
```
#### Specific time schedule format
An attribute with a dynamic value for a specific schedule format must have JSON in the following format:
```javascript
{
"type": "SPECIFIC_TIME",
"daysOfWeek": [
2,
4
],
"endsOn": 0,
"startsOn": 0,
"timezone": "Europe/Kiev"
}
```
<ul>
<li>
<b>timezone:</b> this value is used to designate the timezone you are using.
</li>
<li>
<b>daysOfWeek:</b> this value is used to designate the days in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active.
</li>
<li>
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated days.
</li>
<li>
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified days.
</li>
</ul>
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day.
#### Custom time schedule format
An attribute with a dynamic value for a custom schedule format must have JSON in the following format:
```javascript
{
"type": "CUSTOM"
"timezone": "Europe/Kiev",
"items": [
{
"dayOfWeek": 1,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 2,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 3,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 4,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 5,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 6,
"enabled": true,
"endsOn": 0,
"startsOn": 0
},
{
"dayOfWeek": 7,
"enabled": true,
"endsOn": 0,
"startsOn": 0
}
]
}
```
<ul>
<li>
<b>timezone:</b> this value is used to designate the timezone you are using.
</li>
<li>
<b>items:</b> the array of values representing the days on which the schedule will be active.
</li>
</ul>
One array item contains such fields:
<ul>
<li>
<b>dayOfWeek:</b> this value is used to designate the specified day in numerical representation (Monday - 1, Tuesday 2, etc.) on which the schedule will be active.
</li>
<li>
<b>enabled:</b> this <code>boolean</code> value, used to designate that the specified day in the schedule will be enabled.
</li>
<li>
<b>startsOn:</b> this value is used to designate the timestamp in milliseconds, from which the schedule will be active for the designated day.
</li>
<li>
<b>endsOn:</b> this value is used to designate the timestamp in milliseconds until which the schedule will be active for the specified day.
</li>
</ul>
When <b>startsOn</b> and <b>endsOn</b> equals 0 it's means that the schedule will be active the whole day.

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

@ -1076,6 +1076,7 @@
"argument-asset": "Asset",
"argument-customer": "Customer",
"argument-tenant": "Current tenant",
"argument-owner": "Current owner",
"argument-relation-query": "Related entities",
"argument-type": "Argument type",
"see-debug-events": "See debug events",
@ -1244,6 +1245,137 @@
"metrics": "Defines metrics aggregated based on the configured arguments."
}
},
"alarm-rule": {
"alarm-rules-tab": "Alarm rules",
"alarm-rule": "Alarm rule",
"alarm-rules": "Alarm rules",
"alarm-rules-old": "Old",
"alarm-rules-new": "New",
"severities": "Severities",
"cleared": "Cleared",
"delete-title": "Are you sure you want to delete the alarm rule '{{title}}'?",
"delete-text": "Be careful, after the confirmation the alarm rule and all related data will become unrecoverable.",
"delete-multiple-title": "Are you sure you want to delete { count, plural, =1 {1 alarm rule} other {# alarm rules} }?",
"delete-multiple-text": "Be careful, after the confirmation all selected alarm rules will be removed and all related data will become unrecoverable.",
"create": "Create new alarm rule",
"no-found": "No alarm rules found",
"list": "{ count, plural, =1 {One alarm rule} other {List of # alarm rules} }",
"selected-fields": "{ count, plural, =1 {1 alarm rule} other {# alarm rules} } selected",
"import": "Import alarm rule",
"export": "Export alarm rule",
"export-failed-error": "Unable to export alarm rule: {{error}}",
"alarm-type": "Alarm type",
"alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
"alarm-type-max-length": "Alarm type should be less than 256 characters.",
"clear-alarm": "Clear alarm",
"value-argument": "Argument",
"value-argument-required": "Argument is required.",
"static-settings": "Static settings",
"configuration": "Configuration",
"static-schedule": "Static",
"dynamic-schedule": "Dynamic",
"operation-and": "AND",
"operation-or": "OR",
"condition-during": "During {{during}}",
"condition-during-dynamic": "During \"{{ attribute }}\"",
"condition-repeat-times": "Repeats { count, plural, =1 {1 time} other {# times} }",
"condition-repeat-times-dynamic": "Repeats \"{ attribute }\"",
"filter-preview": "Filter preview",
"condition-settings": "Condition settings",
"static": "Static",
"dynamic": "Dynamic",
"argument-filters": "Argument filters",
"argument-name": "Argument name",
"value-type": "Value type",
"general": "General",
"filter": "Filter",
"operation": "Operation",
"value-source": "Value source",
"value": "Value",
"ignore-case": "Ignore case",
"condition": "Condition",
"script": "Script",
"add-filter": "Add filter",
"edit-filter": "Edit filter",
"conditions": {
"simple": "Simple",
"duration": "Duration",
"repeating": "Repeating"
},
"schedule-title": "Schedule",
"edit-schedule": "Edit alarm schedule",
"schedule-type": "Scheduler type",
"schedule-type-required": "Scheduler type is required.",
"schedule": {
"any-time": "Active all the time",
"specific-time": "Active at a specific time",
"custom": "Custom"
},
"schedule-day": {
"monday": "Monday",
"tuesday": "Tuesday",
"wednesday": "Wednesday",
"thursday": "Thursday",
"friday": "Friday",
"saturday": "Saturday",
"sunday": "Sunday"
},
"schedule-days": "Days",
"schedule-time": "Time",
"schedule-time-from": "From",
"schedule-time-to": "To",
"schedule-days-of-week-required": "At least one day of week should be selected.",
"expression-type": {
"simple": "Simple",
"tbel": "TBEL"
},
"operation-type": {
"and": "And",
"or": "Or"
},
"filter-predicate-type": {
"string": "String",
"numeric": "Numeric",
"boolean": "Boolean",
"complex": "Complex"
},
"alarm-rule-additional-info": "Additional info",
"edit-alarm-rule-additional-info": "Edit additional info",
"alarm-rule-additional-info-placeholder": "Please provide your comments and adjustments here to display them within Alarm details under Additional info",
"alarm-rule-additional-info-hint": "Hint: use <code><span style=\"color: #000;\">$&#123;</span>Argument name<span style=\"color: #000;\">&#125;</span></code> to substitute values of the arguments that are used in alarm rule condition.",
"alarm-rule-mobile-dashboard": "Mobile dashboard",
"alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard",
"alarm-rule-no-mobile-dashboard": "No dashboard selected",
"alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Please add alarm rule condition",
"edit-alarm-rule-condition": "Edit alarm rule condition",
"condition-type": "Condition type",
"select-alarm-severity": "Select alarm severity",
"add-create-alarm-rule-prompt": "Please add create alarm rule",
"add-create-alarm-rule": "Add create condition",
"add-clear-alarm-rule": "Add clear condition",
"condition-duration": "Condition duration",
"condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit",
"condition-duration-value-range": "Duration value should be in a range from 1 to 2147483647.",
"condition-duration-value-pattern": "Duration value should be integers.",
"condition-duration-value-required": "Duration value is required.",
"condition-duration-time-unit-required": "Time unit is required.",
"condition-repeating-value": "Count of events",
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"create-alarm-rules": "Create alarm rules",
"clear-alarm-rule": "Clear alarm rule",
"no-clear-alarm-rule": "No clear condition configured",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities",
"alarm-rule-relation-types-list": "Relation types",
"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"
},
"ai-models": {
"ai-models": "AI models",
"ai-model": "AI model",

Loading…
Cancel
Save