Browse Source

Merge pull request #14360 from ArtemDzhereleiko/AD/imp/alarm-rule-page

Alarm rules page
pull/14328/head
Vladyslav Prykhodko 10 months ago
committed by GitHub
parent
commit
077847d44e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  2. 6
      ui-ngx/src/app/core/http/entity.service.ts
  3. 32
      ui-ngx/src/app/core/services/menu.models.ts
  4. 31
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  5. 14
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  6. 94
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html
  7. 58
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss
  8. 296
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  9. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html
  10. 21
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss
  11. 44
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts
  12. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  13. 51
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  14. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
  15. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
  16. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  17. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  18. 16
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  19. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  20. 21
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  21. 65
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss
  22. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  23. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  24. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  25. 2
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts
  26. 11
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  27. 11
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  28. 44
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  29. 12
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  30. 23
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  31. 2
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.html
  32. 5
      ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts
  33. 7
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  34. 7
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  35. 21
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -15,12 +15,18 @@
///
import { Injectable } from '@angular/core';
import { createDefaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import {
createDefaultHttpOptions,
defaultHttpOptionsFromConfig,
defaultHttpOptionsFromParams,
RequestConfig
} from './http-utils';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageData } from '@shared/models/page/page-data';
import {
CalculatedField,
CalculatedFieldsQuery,
CalculatedFieldTestScriptInputParams,
CalculatedFieldType
} from '@shared/models/calculated-field.models';
@ -50,8 +56,12 @@ export class CalculatedFieldsService {
return this.http.delete<boolean>(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config));
}
public getCalculatedFieldsFilter(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config));
}
public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable<PageData<CalculatedField>> {
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config));
return this.http.get<PageData<CalculatedField>>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type} , config));
}
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
@ -61,4 +71,8 @@ export class CalculatedFieldsService {
public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable<CalculatedFieldEventBody> {
return this.http.get<CalculatedFieldEventBody>(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config));
}
public getAlarmRuleNames(pageLink: PageLink, type: CalculatedFieldType, config?: RequestConfig): Observable<PageData<string>> {
return this.http.get<PageData<string>>(`/api/calculatedFields/names${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type}, config));
}
}

6
ui-ngx/src/app/core/http/entity.service.ts

@ -189,6 +189,12 @@ export class EntityService {
case EntityType.AI_MODEL:
observable = this.aiModelService.getAiModelById(entityId, config);
break;
case EntityType.DEVICE_PROFILE:
observable = this.deviceProfileService.getDeviceProfile(entityId, config);
break;
case EntityType.ASSET_PROFILE:
observable = this.assetProfileService.getAssetProfile(entityId, config);
break;
}
return observable;
}

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

@ -85,7 +85,9 @@ export enum MenuId {
domains = 'domains',
clients = 'clients',
audit_log = 'audit_log',
alarms_center = 'alarms_center',
alarms = 'alarms',
alarm_rules = 'alarm_rules',
dashboards = 'dashboards',
entities = 'entities',
devices = 'devices',
@ -494,16 +496,36 @@ export const menuSectionMap = new Map<MenuId, MenuSection>([
icon: 'track_changes'
}
],
[
MenuId.alarms_center,
{
id: MenuId.alarms_center,
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline'
}
],
[
MenuId.alarms,
{
id: MenuId.alarms,
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
path: '/alarms/alarms',
icon: 'mdi:alert-outline'
}
],
[
MenuId.alarm_rules,
{
id: MenuId.alarm_rules,
name: 'alarm-rule.alarm-rules',
type: 'link',
path: '/alarms/alarm-rules',
icon: 'tune'
}
],
[
MenuId.dashboards,
{
@ -792,7 +814,13 @@ const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
Authority.TENANT_ADMIN,
[
{id: MenuId.home},
{id: MenuId.alarms},
{
id: MenuId.alarms_center,
pages: [
{id: MenuId.alarms},
{id: MenuId.alarm_rules}
]
},
{id: MenuId.dashboards},
{
id: MenuId.entities,

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex size-full max-w-4xl flex-col">
<div [formGroup]="fieldFormGroup" class="calculated-field-dialog-container flex h-full flex-col">
<mat-toolbar color="primary">
<h2>{{ 'alarm-rule.alarm-rule' | translate}}</h2>
<span class="flex-1"></span>
@ -47,30 +47,53 @@
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
entityLabel="alarm-rule.alarm-rule"
[class.mb-5]="fieldFormGroup.get('name').errors && fieldFormGroup.get('name').touched"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
@if (!data.entityId) {
<div class="flex flex-col gap-4" formGroupName="entityId">
<tb-entity-type-select appearance="outline"
subscriptSizing="dynamic"
showLabel
label="{{ 'alarm-rule.target-entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="alarmRuleEntityTypeList"
formControlName="entityType">
</tb-entity-type-select>
@if (fieldFormGroup.get('entityId.entityType').value) {
<tb-entity-autocomplete
formControlName="id"
appearance="outline"
subscriptSizing="dynamic"
[placeholder]="'action.set' | translate"
[required]="true"
[entityType]="fieldFormGroup.get('entityId.entityType').value"
/>
}
</div>
}
</div>
<ng-container formGroupName="configuration">
<div class="tb-form-panel">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="data.entityId"
[entityId]="data.entityId || fieldFormGroup.get('entityId').value"
[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="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | 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="tb-form-panel-title">{{ 'alarm-rule.clear-condition' | 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">

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

@ -23,7 +23,7 @@ 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 { AliasEntityType, EntityType, entityTypeTranslations } 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';
@ -57,13 +57,17 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]],
type: [CalculatedFieldType.ALARM],
debugSettings: [],
entityId: this.fb.group({
entityType: this.fb.control<EntityType | AliasEntityType | null>(null, Validators.required),
id: ['', Validators.required],
}),
configuration: this.fb.group({
arguments: this.fb.control({}),
propagate: [false],
propagateToOwner: [false],
propagateToTenant: [false],
propagateRelationTypes: [null],
createRules: [null],
createRules: [null, Validators.required],
clearRule: [null],
}),
});
@ -74,6 +78,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
} : null;
readonly EntityType = EntityType;
readonly entityTypeTranslations = entityTypeTranslations;
readonly alarmRuleEntityTypeList = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE];
readonly CalculatedFieldType = CalculatedFieldType;
readonly ScriptLanguage = ScriptLanguage;
@ -160,8 +166,8 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}
private applyDialogData(): void {
const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
const { configuration = {}, type = CalculatedFieldType.ALARM, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false});
}
private observeIsLoading(): void {

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

@ -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.
-->
<ng-container *ngIf="panelMode; else componentMode">
<ng-container *ngTemplateOutlet="alarmRuleFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else alarmFilter">
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
mat-stroked-button
(click)="toggleAlarmRuleFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
<ng-template #alarmRuleFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
<div class="tb-panel-actions flex flex-row items-center justify-end">
<button type="button"
mat-button
color="primary"
(click)="reset()">
{{ 'action.reset' | translate }}
</button>
<span class="flex-1"></span>
<button type="button"
mat-button
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button type="submit"
mat-raised-button
color="primary"
[disabled]="alarmRuleFilterConfigForm.invalid || !alarmRuleFilterConfigForm.dirty">
{{ 'action.update' | translate }}
</button>
</div>
</form>
</ng-template>
<ng-template #alarmFilter>
<div class="tb-form-panel tb-alarm-filter-config-component no-padding no-border" [formGroup]="alarmRuleFilterConfigForm">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.alarm-rule-type-list</div>
<tb-entity-subtype-list subscriptSizing="dynamic"
formControlName="name"
appearance="outline"
[additionalClasses]="['tb-chips', 'flex']"
[entityType]="entityType.CALCULATED_FIELD">
</tb-entity-subtype-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
<mat-option *ngFor="let type of listEntityTypes" [value]="type">
{{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
@if (alarmRuleFilterConfigForm.get('entityType').value) {
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.alarm-rule-entity-list</div>
<tb-entity-list appearance="outline"
subscriptSizing="dynamic"
class="flex flex-1"
inlineField
syncIdsWithDB
labelText="{{'entity.entity-list' | translate}}"
[entityType]="alarmRuleFilterConfigForm.get('entityType').value"
formControlName="entities">
</tb-entity-list>
</div>
}
</div>
</ng-template>

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

@ -0,0 +1,58 @@
/**
* 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 '../scss/constants';
:host {
display: flex;
max-width: 100%;
.mdc-button {
max-width: 100%;
}
}
:host ::ng-deep {
.mdc-button {
.mat-icon {
min-width: 24px;
}
.mdc-button__label {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
::ng-deep {
.tb-alarm-filter-config-component {
max-width: 100%;
width: 600px;
min-width: 100%;
flex: 1;
tb-entity-subtype-list {
flex: 1;
@media #{$mat-gt-xs} {
width: 180px;
}
.mdc-evolution-chip-set__chips {
width: 100%;
}
}
}
}

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

@ -0,0 +1,296 @@
///
/// 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,
ElementRef,
forwardRef,
Inject,
InjectionToken,
Input,
OnInit,
Optional,
TemplateRef,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { TranslateService } from '@ngx-translate/core';
import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { fromEvent, Subscription } from 'rxjs';
import { POSITION_MAP } from '@shared/models/overlay.models';
import { UtilsService } from '@core/services/utils.service';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AlarmRuleFilterConfig } from "@shared/models/alarm-rule.models";
export const ALARM_FILTER_CONFIG_DATA = new InjectionToken<any>('AlarmRuleFilterConfigData');
export interface AlarmRuleFilterConfigData {
panelMode: boolean;
userMode: boolean;
alarmRuleFilterConfig: AlarmRuleFilterConfig;
initialAlarmRuleFilterConfig?: AlarmRuleFilterConfig;
}
@Component({
selector: 'tb-alarm-rule-filter-config',
templateUrl: './alarm-rule-filter-config.component.html',
styleUrls: ['./alarm-rule-filter-config.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AlarmRuleFilterConfigComponent),
multi: true
}
]
})
export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAccessor {
@ViewChild('alarmRuleFilterPanel')
alarmRuleFilterPanel: TemplateRef<any>;
@Input() disabled: boolean;
@coerceBoolean()
@Input()
buttonMode = true;
@coerceBoolean()
@Input()
userMode = false;
@coerceBoolean()
@Input()
propagatedFilter = true;
@Input()
initialAlarmRuleFilterConfig: AlarmRuleFilterConfig = {
name: [],
entityType: null,
entities: []
};
panelMode = false;
buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter');
alarmRuleFilterConfigForm: FormGroup;
alarmFilterOverlayRef: OverlayRef;
panelResult: AlarmRuleFilterConfig = null;
entityType = EntityType;
listEntityTypes = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE];
entityTypeTranslations = entityTypeTranslations;
private alarmRuleFilterConfig: AlarmRuleFilterConfig;
private resizeWindows: Subscription;
private propagateChange = (_: any) => {};
constructor(@Optional() @Inject(ALARM_FILTER_CONFIG_DATA)
private data: AlarmRuleFilterConfigData | undefined,
@Optional()
private overlayRef: OverlayRef,
private fb: FormBuilder,
private translate: TranslateService,
private overlay: Overlay,
private nativeElement: ElementRef,
private viewContainerRef: ViewContainerRef,
private utils: UtilsService,
private destroyRef: DestroyRef) {
}
ngOnInit(): void {
if (this.data) {
this.panelMode = this.data.panelMode;
this.userMode = this.data.userMode;
this.alarmRuleFilterConfig = this.data.alarmRuleFilterConfig;
this.initialAlarmRuleFilterConfig = this.data.initialAlarmRuleFilterConfig;
if (this.panelMode && !this.initialAlarmRuleFilterConfig) {
this.initialAlarmRuleFilterConfig = deepClone(this.alarmRuleFilterConfig);
}
}
this.alarmRuleFilterConfigForm = this.fb.group({
name: [null, []],
entityType: [null, []],
entities: [null, []]
});
this.alarmRuleFilterConfigForm.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(
() => {
if (!this.buttonMode) {
this.alarmRuleConfigUpdated(this.alarmRuleFilterConfigForm.value);
}
}
);
if (this.panelMode) {
this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.alarmRuleFilterConfigForm.disable({emitEvent: false});
} else {
this.alarmRuleFilterConfigForm.enable({emitEvent: false});
}
}
writeValue(alarmRuleFilterConfig?: AlarmRuleFilterConfig): void {
this.alarmRuleFilterConfig = alarmRuleFilterConfig;
if (!this.initialAlarmRuleFilterConfig && alarmRuleFilterConfig) {
this.initialAlarmRuleFilterConfig = deepClone(alarmRuleFilterConfig);
}
this.updateButtonDisplayValue();
this.updateAlarmRuleConfigForm(alarmRuleFilterConfig);
}
toggleAlarmRuleFilterPanel($event: Event) {
if ($event) {
$event.stopPropagation();
}
const config = new OverlayConfig({
panelClass: 'tb-filter-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: true,
maxHeight: '80vh',
height: 'min-content',
minWidth: ''
});
config.hasBackdrop = true;
config.positionStrategy = this.overlay.position()
.flexibleConnectedTo(this.nativeElement)
.withPositions([POSITION_MAP.bottomLeft]);
this.alarmFilterOverlayRef = this.overlay.create(config);
this.alarmFilterOverlayRef.backdropClick().subscribe(() => {
this.alarmFilterOverlayRef.dispose();
});
this.alarmFilterOverlayRef.attach(new TemplatePortal(this.alarmRuleFilterPanel,
this.viewContainerRef));
this.resizeWindows = fromEvent(window, 'resize').subscribe(() => {
this.alarmFilterOverlayRef.updatePosition();
});
}
cancel() {
this.updateAlarmRuleConfigForm(this.alarmRuleFilterConfig);
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.resizeWindows.unsubscribe();
this.alarmFilterOverlayRef.dispose();
}
}
update() {
this.alarmRuleConfigUpdated(this.alarmRuleFilterConfigForm.value);
this.alarmRuleFilterConfigForm.markAsPristine();
if (this.panelMode) {
this.panelResult = this.alarmRuleFilterConfig;
}
if (this.overlayRef) {
this.overlayRef.dispose();
} else {
this.resizeWindows.unsubscribe();
this.alarmFilterOverlayRef.dispose();
}
}
reset() {
const alarmRuleFilterConfig = this.alarmRuleFilterConfigFromFormValue(this.alarmRuleFilterConfigForm.value);
if (!this.alarmRuleFilterConfigEquals(alarmRuleFilterConfig, this.initialAlarmRuleFilterConfig)) {
this.updateAlarmRuleConfigForm(this.initialAlarmRuleFilterConfig);
this.alarmRuleFilterConfigForm.markAsDirty();
}
}
private alarmRuleFilterConfigEquals = (filter1?: AlarmRuleFilterConfig, filter2?: AlarmRuleFilterConfig): boolean => {
if (filter1 === filter2) {
return true;
}
if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) {
return true;
} else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) {
if (!isArraysEqualIgnoreUndefined(filter1.name, filter2.name)) {
return false;
}
if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) {
return false;
}
return filter1.entityType !== filter2.entityType;
}
return false;
};
private updateAlarmRuleConfigForm(alarmRuleFilterConfig?: AlarmRuleFilterConfig) {
this.alarmRuleFilterConfigForm.patchValue({
name: alarmRuleFilterConfig?.name ?? [],
entityType: alarmRuleFilterConfig?.entityType ?? null,
entities: alarmRuleFilterConfig?.entities ?? [],
}, {emitEvent: false});
}
private alarmRuleConfigUpdated(formValue: any) {
this.alarmRuleFilterConfig = this.alarmRuleFilterConfigFromFormValue(formValue);
this.updateButtonDisplayValue();
this.propagateChange(this.alarmRuleFilterConfig);
}
private alarmRuleFilterConfigFromFormValue(formValue: any): AlarmRuleFilterConfig {
return {
name: formValue?.name ?? [],
entityType: formValue?.entityType ?? null,
entities: formValue?.entities ?? [],
};
}
private updateButtonDisplayValue() {
if (this.buttonMode) {
const filterTextParts: string[] = [];
if (this.alarmRuleFilterConfig?.name?.length) {
filterTextParts.push(this.alarmRuleFilterConfig.name.map((type) => this.customTranslate(type)).join(', '));
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
} else {
this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title') + `: ${filterTextParts.join(', ')}`;
}
}
}
private customTranslate(entity: string) {
return this.utils.customTranslation(entity, entity);
}
}

20
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.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.
-->
<tb-alarm-rule-filter-config [ngModel]="alarmRuleTableConfig.alarmRuleFilterConfig"
(ngModelChange)="alarmRuleFilterChanged($event)">
</tb-alarm-rule-filter-config>

21
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss

@ -0,0 +1,21 @@
/**
* 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 {
padding-right: 8px;
overflow: hidden;
max-width: 100%;
}

44
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts

@ -0,0 +1,44 @@
///
/// 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 } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '../../components/entity/entity-table-header.component';
import { AlarmFilterConfig } from '@shared/models/query/query.models';
import { CalculatedFieldAlarmRule, CalculatedFieldsQuery } from "@shared/models/calculated-field.models";
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
@Component({
selector: 'tb-alarm-rule-table-header',
templateUrl: './alarm-rule-table-header.component.html',
styleUrls: ['./alarm-rule-table-header.component.scss']
})
export class AlarmRuleTableHeaderComponent extends EntityTableHeaderComponent<CalculatedFieldAlarmRule> {
get alarmRuleTableConfig(): AlarmRulesTableConfig {
return this.entitiesTableConfig as AlarmRulesTableConfig;
}
constructor(protected store: Store<AppState>) {
super(store);
}
alarmRuleFilterChanged(alarmRuleFilterConfig: CalculatedFieldsQuery) {
this.alarmRuleTableConfig.alarmRuleFilterConfig = alarmRuleFilterConfig;
this.alarmRuleTableConfig.getTable().resetSortAndFilter(true, true);
}
}

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

@ -48,6 +48,8 @@ import {
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";
import { AlarmRuleFilterConfigComponent } from "@home/components/alarm-rules/alarm-rule-filter-config.component";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
@NgModule({
declarations: [
@ -66,6 +68,8 @@ import { AlarmRuleDetailsDialogComponent } from "@home/components/alarm-rules/al
AlarmRuleFilterPredicateValueComponent,
AlarmRuleComplexFilterPredicateDialogComponent,
AlarmRuleDetailsDialogComponent,
AlarmRuleFilterConfigComponent,
AlarmRuleTableHeaderComponent
],
imports: [
CommonModule,

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

@ -17,10 +17,11 @@
import {
checkBoxCell,
DateEntityTableColumn,
EntityLinkTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityType, entityTypeTranslations } 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';
@ -40,6 +41,7 @@ import {
ArgumentEntityType,
CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldsQuery,
CalculatedFieldType,
} from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.service';
@ -55,6 +57,8 @@ import {
} from "@home/components/calculated-fields/components/debug-dialog/calculated-field-debug-dialog.component";
import { AlarmSeverity, alarmSeverityTranslations } from "@shared/models/alarm.models";
import { UtilsService } from "@core/services/utils.service";
import { deepClone, getEntityDetailsPageURL } from "@core/utils";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
export class AlarmRulesTableConfig extends EntityTableConfig<any> {
@ -64,6 +68,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
};
alarmRuleFilterConfig: CalculatedFieldsQuery;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
@ -77,11 +83,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
public pageMode: boolean = false,
) {
super();
this.tableTitle = this.translate.instant('alarm-rule.alarm-rules');
if (this.pageMode) {
this.headerComponent = AlarmRuleTableHeaderComponent;
}
this.tableTitle = this.pageMode ? '' : 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',
@ -117,14 +126,29 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
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>('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%',
entity => this.utilsService.customTranslation(entity.name, entity.name)));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', '67%',
if (this.pageMode) {
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('entityType', 'alarm-rule.entity-type', '15%',
entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)));
this.columns.push(new EntityLinkTableColumn<CalculatedFieldAlarmRule>('entityName', 'alarm-rule.entity-name', '30%',
entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']),
entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false));
}
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%',
entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '),
() => ({}), false));
this.columns.push(new EntityTableColumn<CalculatedFieldAlarmRule>('clearRule', 'alarm-rule.cleared', '70px',
entity => checkBoxCell(!!entity.configuration.clearRule), ()=> { return {padding: 0, textAlign: 'center'}}, false));
this.cellActionDescriptors.push(
{
name: this.translate.instant('notification.copy-template'),
icon: 'content_copy',
isEnabled: () => true,
onAction: ($event, entity) => this.copyCalculatedField(entity)
}
);
this.cellActionDescriptors.push(
{
name: this.translate.instant('action.export'),
@ -156,7 +180,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
}
fetchCalculatedFields(pageLink: PageLink): Observable<PageData<CalculatedField>> {
return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM);
return this.pageMode ?
this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {type: CalculatedFieldType.ALARM, ...this.alarmRuleFilterConfig}) :
this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM);
}
onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void {
@ -195,6 +221,17 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
});
}
private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
const copyCalculatedAlarmRule = deepClone(calculatedField);
copyCalculatedAlarmRule.entityId = null;
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,
@ -205,7 +242,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
entityId: this.entityId,
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig,
isDirty,
},

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

@ -16,5 +16,5 @@
-->
@if (alarmRulesTableConfig) {
<tb-entities-table [entitiesTableConfig]="alarmRulesTableConfig"></tb-entities-table>
<tb-entities-table [class.white-background]="!pageMode" [entitiesTableConfig]="alarmRulesTableConfig"></tb-entities-table>
}

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

@ -14,7 +14,7 @@
* limitations under the License.
*/
:host ::ng-deep {
tb-entities-table {
.white-background {
--mat-sidenav-content-background-color: white;
}
}

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

@ -36,6 +36,7 @@ import { EntityDebugSettingsService } from '@home/components/entity/debug/entity
import { DatePipe } from '@angular/common';
import { AlarmRulesTableConfig } from "@home/components/alarm-rules/alarm-rules-table-config";
import { UtilsService } from "@core/services/utils.service";
import { ActivatedRoute } from "@angular/router";
@Component({
selector: 'tb-alarm-rules-table',
@ -55,6 +56,8 @@ export class AlarmRulesTableComponent {
alarmRulesTableConfig: AlarmRulesTableConfig;
pageMode: boolean = false;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
@ -65,10 +68,12 @@ export class AlarmRulesTableComponent {
private importExportService: ImportExportService,
private entityDebugSettingsService: EntityDebugSettingsService,
private utilsService: UtilsService,
private destroyRef: DestroyRef) {
private destroyRef: DestroyRef,
private route: ActivatedRoute,
) {
this.pageMode = !!this.route.snapshot.data.isPage;
effect(() => {
if (this.active()) {
if (this.active() || this.pageMode) {
this.alarmRulesTableConfig = new AlarmRulesTableConfig(
this.calculatedFieldsService,
this.translate,
@ -83,6 +88,7 @@ export class AlarmRulesTableComponent {
this.importExportService,
this.entityDebugSettingsService,
this.utilsService,
this.pageMode
);
this.cd.markForCheck();
}

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

@ -22,7 +22,7 @@
<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-option [value]="AlarmRuleExpressionType.TBEL">{{ 'alarm-rule.expression-type.script' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<button mat-icon-button
@ -57,6 +57,7 @@
</div>
<tb-js-func formControlName="expression"
functionName="expression"
class="tb-alarm-rule-script"
[disableUndefinedCheck]="true"
[scriptLanguage]="scriptLanguage.TBEL"
[functionArgs]="functionArgs"
@ -64,7 +65,7 @@
[editorCompleter]="argumentsEditorCompleter"
noValidate="true">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.tbel' | translate }}
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.script' | translate }}
</div>
</tb-js-func>
</div>

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

@ -74,12 +74,16 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
@coerceBoolean()
disabled: boolean;
@Input()
@coerceBoolean()
required: boolean;
@Input()
arguments: Record<string, CalculatedFieldArgument>;
alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'],
expression: [{type: AlarmRuleExpressionType.SIMPLE}, Validators.required],
expression: [{type: AlarmRuleExpressionType.SIMPLE}],
schedule: [null],
});
@ -114,19 +118,21 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
}
writeValue(value: AlarmRuleCondition): void {
this.modelValue = value;
this.updateConditionInfo();
if (value) {
this.modelValue = value;
this.updateConditionInfo();
}
}
public conditionSet() {
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters);
return this.modelValue && (this.modelValue.expression?.expression || this.modelValue.expression?.filters) || !this.required;
}
public validate(c: UntypedFormControl) {
return this.conditionSet() ? null : {
alarmRuleCondition: {
valid: false,
},
}
};
}

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

@ -16,7 +16,7 @@
-->
<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 formControlName="condition" [arguments]="arguments" [required]="required">
</tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs">

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

@ -21,14 +21,13 @@ import {
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
Validator,
Validators
Validator
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
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 { AlarmRule, AlarmRuleCondition } from "@shared/models/alarm-rule.models";
import { CalculatedFieldArgument } from "@shared/models/calculated-field.models";
import {
AlarmRuleDetailsDialogComponent,
@ -69,7 +68,7 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
private modelValue: AlarmRule;
alarmRuleFormGroup = this.fb.group({
condition: this.fb.control({}, Validators.required),
condition: this.fb.control<AlarmRuleCondition | null>(null),
alarmDetails: [null],
dashboardId: [null]
});
@ -106,12 +105,14 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
}
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});
if (value) {
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) {

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

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

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

@ -55,6 +55,7 @@
</div>
<div *ngIf="!disabled">
<button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'alarm-rule.add-create-alarm-rule' | translate }}"

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

@ -157,7 +157,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
}
public validate(c: UntypedFormControl) {
return (this.createAlarmRulesFormGroup.valid) ? null : {
return (this.createAlarmRulesFormArray().length && this.createAlarmRulesFormGroup.valid) ? null : {
createAlarmRules: {
valid: false,
},

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

@ -271,7 +271,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
return acc;
}, {} as Record<EntityType, string[]>);
const tasks = Object.entries(entitiesByType).map(([entityType, ids]) =>
this.entityService.getEntities(entityType as EntityType, ids)
this.entityService.getEntities(entityType as EntityType, ids, {ignoreLoading: true})
);
if (!tasks.length) {
return;

2
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-button.component.ts

@ -61,6 +61,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
@Input() additionalActionConfig: AdditionalDebugActionConfig;
@Input({required: true}) entityType: EntityType;
@Input() entityLabel: string;
debugSettingsFormGroup = this.fb.group({
failuresEnabled: [false],
@ -124,6 +125,7 @@ export class EntityDebugSettingsButtonComponent implements ControlValueAccessor
debugConfig: {
maxDebugModeDuration: this.maxDebugModeDuration,
entityType: this.entityType,
entityLabel: this.entityLabel,
additionalActionConfig: this.additionalActionConfig,
},
onSettingsAppliedFn: settings => {

11
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -112,17 +112,6 @@
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms"
[deviceProfileId]="null">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="provisionConfigFormGroup" [optional]="true">
<form [formGroup]="provisionConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.device-provisioning' | translate }}</ng-template>

11
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts

@ -85,8 +85,6 @@ export class AddDeviceProfileDialogComponent extends
transportConfigFormGroup: UntypedFormGroup;
alarmRulesFormGroup: UntypedFormGroup;
provisionConfigFormGroup: UntypedFormGroup;
serviceType = ServiceType.TB_RULE_ENGINE;
@ -132,12 +130,6 @@ export class AddDeviceProfileDialogComponent extends
this.deviceProfileTransportTypeChanged();
});
this.alarmRulesFormGroup = this.fb.group(
{
alarms: [null]
}
);
this.provisionConfigFormGroup = this.fb.group(
{
provisionConfiguration: [{
@ -176,8 +168,6 @@ export class AddDeviceProfileDialogComponent extends
case 1:
return this.transportConfigFormGroup;
case 2:
return this.alarmRulesFormGroup;
case 3:
return this.provisionConfigFormGroup;
}
}
@ -199,7 +189,6 @@ export class AddDeviceProfileDialogComponent extends
profileData: {
configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
alarms: this.alarmRulesFormGroup.get('alarms').value,
provisionConfiguration: deviceProvisionConfiguration
}
};

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

@ -22,6 +22,8 @@ import { OAuth2Service } from '@core/http/oauth2.service';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models';
import { RouterTabsComponent } from "@home/components/router-tabs.component";
import { AlarmRulesTableComponent } from "@home/components/alarm-rules/alarm-rules-table.component";
@Injectable()
export class OAuth2LoginProcessingUrlResolver {
@ -37,16 +39,48 @@ export class OAuth2LoginProcessingUrlResolver {
const routes: Routes = [
{
path: 'alarms',
component: AlarmTableComponent,
component: RouterTabsComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'alarm.alarms',
breadcrumb: {
menuId: MenuId.alarms
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
redirectTo: '/alarms/alarms'
}
},
isPage: true,
alarmsMode: AlarmsMode.ALL
}
{
path: 'alarms',
component: AlarmTableComponent,
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'alarm.alarms',
breadcrumb: {
menuId: MenuId.alarms
},
isPage: true,
alarmsMode: AlarmsMode.ALL
}
},
{
path: 'alarm-rules',
component: AlarmRulesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'alarm-rule.alarm-rules',
breadcrumb: {
menuId: MenuId.alarm_rules
},
isPage: true,
}
}
]
}
];

12
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -301,6 +301,18 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
this.entityRequiredText = 'ai-models.model-required';
this.notFoundEntities = 'ai-models.no-model-text';
break;
case EntityType.DEVICE_PROFILE:
this.entityText = 'device-profile.device-profile';
this.noEntitiesMatchingText = 'device-profile.no-device-profiles-matching';
this.entityRequiredText = 'device-profile.device-profile-required';
this.notFoundEntities = 'device-profile.no-device-profiles-text';
break;
case EntityType.ASSET_PROFILE:
this.entityText = 'asset-profile.asset-profile';
this.noEntitiesMatchingText = 'asset-profile.no-asset-profiles-matching';
this.entityRequiredText = 'asset-profile.asset-profile-required';
this.notFoundEntities = 'asset-profile.no-asset-profiles-text';
break;
case AliasEntityType.CURRENT_CUSTOMER:
this.entityText = 'customer.default-customer';
this.noEntitiesMatchingText = 'customer.no-customers-matching';

23
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts

@ -31,6 +31,8 @@ import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
import { CalculatedFieldType } from "@shared/models/calculated-field.models";
import { CalculatedFieldsService } from "@core/http/calculated-fields.service";
@Component({
selector: 'tb-entity-subtype-list',
@ -77,6 +79,9 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
@Input()
entityType: EntityType;
@Input()
calculatedFieldType: CalculatedFieldType;
@Input()
emptyInputPlaceholder: string;
@ -117,12 +122,14 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
private propagateChange = (v: any) => { };
private hasPageDataEntitySubTypes = new Set<EntityType>([
EntityType.ALARM
EntityType.ALARM,
EntityType.CALCULATED_FIELD
]);
constructor(private broadcast: BroadcastService,
public translate: TranslateService,
private alarmService: AlarmService,
private calculatedFieldsService: CalculatedFieldsService,
private utils: UtilsService,
private fb: FormBuilder,
private entityService: EntityService) {
@ -194,6 +201,13 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
this.noSubtypesMathingText = 'alarm.no-alarm-types-matching';
this.subtypeListEmptyText = 'alarm.alarm-type-list-empty';
break;
case EntityType.CALCULATED_FIELD:
this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-rule-type')
: this.translate.instant('alarm-rule.any-type');
this.secondaryPlaceholder = '+' + this.translate.instant('alarm-rule.alarm-rule');
this.noSubtypesMathingText = 'alarm-rule.no-alarm-rule-types-matching';
this.subtypeListEmptyText = 'alarm-rule.alarm-rule-type-list-empty';
break;
}
if (this.emptyInputPlaceholder) {
@ -315,15 +329,22 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
if (this.hasPageDataEntitySubTypes.has(this.entityType)) {
const pageLink = new PageLink(25, 0, searchText);
let subTypesPagesObservable: Observable<PageData<EntitySubtype>>;
let subTypesCfPagesObservable: Observable<PageData<string>>;
switch (this.entityType) {
case EntityType.ALARM:
subTypesPagesObservable = this.alarmService.getAlarmTypes(pageLink, {ignoreLoading: true});
break;
case EntityType.CALCULATED_FIELD:
subTypesCfPagesObservable = this.calculatedFieldsService.getAlarmRuleNames(pageLink, CalculatedFieldType.ALARM, {ignoreLoading: true});
}
if (subTypesPagesObservable) {
this.entitySubtypes = subTypesPagesObservable.pipe(
map(subTypesPage => subTypesPage.data.map(subType => subType.type)),
);
} else if (subTypesCfPagesObservable) {
this.entitySubtypes = subTypesCfPagesObservable.pipe(
map(subTypesPage => subTypesPage.data)
);
} else {
return throwError(null);
}

2
ui-ngx/src/app/shared/components/entity/entity-type-select.component.html

@ -16,7 +16,7 @@
-->
<mat-form-field [formGroup]="entityTypeFormGroup"
[subscriptSizing]="inlineField ? 'dynamic' : 'fixed'"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"
[appearance]="inlineField ? 'outline' : appearance">
<mat-label *ngIf="showLabel && !inlineField">{{ label }}</mat-label>
<mat-select [required]="required" formControlName="entityType">

5
ui-ngx/src/app/shared/components/entity/entity-type-select.component.ts

@ -21,7 +21,7 @@ import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared
import { EntityService } from '@core/http/entity.service';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatFormFieldAppearance } from '@angular/material/form-field';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-entity-type-select',
@ -68,6 +68,9 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
@coerceBoolean()
inlineField: boolean;

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

@ -26,6 +26,7 @@ import {
NumericOperation,
StringOperation
} from "@shared/models/query/query.models";
import { EntityType } from "@shared/models/entity-type.models";
export enum AlarmRuleScheduleType {
ANY_TIME = 'ANY_TIME',
@ -150,3 +151,9 @@ export interface BaseComplexFilterPredicate<T extends AlarmRuleFilterPredicate>
}
export type ComplexAlarmRuleFilterPredicate = BaseComplexFilterPredicate<AlarmRuleFilterPredicate>;
export interface AlarmRuleFilterConfig {
name?: Array<string>;
entityType?: EntityType;
entities?: Array<string>;
}

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

@ -1051,3 +1051,10 @@ export function uniqueNameValidator(existingNames: string[]): ValidatorFn {
return namesSet.has(newName) ? { duplicateName: true } : null;
};
}
export interface CalculatedFieldsQuery {
type: CalculatedFieldType;
entityType?: EntityType;
entities?: Array<string>;
name?: string;
}

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

@ -1360,12 +1360,18 @@
"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",
"add": "Add alarm rule",
"copy": "Copy alarm rule configuration",
"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}}",
"entity-type": "Entity type",
"target-entity-type": "Target entity type",
"entity-type-required": "Entity type is required.",
"entity-name": "Entity name",
"alarm-type": "Alarm type",
"alarm-type-required": "Alarm type is required.",
"alarm-type-pattern": "Alarm type is invalid.",
@ -1430,7 +1436,7 @@
"schedule-days-of-week-required": "At least one day of week should be selected.",
"expression-type": {
"simple": "Simple",
"tbel": "TBEL"
"script": "Script"
},
"operation-type": {
"and": "And",
@ -1468,8 +1474,8 @@
"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",
"create-conditions": "Create conditions",
"clear-condition": "Clear condition",
"no-clear-alarm-rule": "No clear condition configured",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities",
@ -1477,7 +1483,14 @@
"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",
"debugging": "Alarm rule debugging"
"alarm-rule-filter-title": "Filter",
"debugging": "Alarm rule debugging",
"any-type": "Any type",
"enter-alarm-rule-type": "Enter alarm rule type",
"no-alarm-rule-types-matching": "No alarm rule types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm rule types selected.",
"alarm-rule-type-list": "Alarm rule type list",
"alarm-rule-entity-list": "Entity list"
},
"ai-models": {
"ai-models": "AI models",

Loading…
Cancel
Save