Browse Source

UI: Add alarm rule page

pull/14360/head
ArtemDzhereleiko 11 months ago
parent
commit
dfab62dbda
  1. 9
      ui-ngx/src/app/core/http/calculated-fields.service.ts
  2. 32
      ui-ngx/src/app/core/services/menu.models.ts
  3. 30
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  4. 14
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  5. 94
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html
  6. 59
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss
  7. 301
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  8. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html
  9. 21
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss
  10. 44
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts
  11. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts
  12. 50
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  13. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html
  14. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss
  15. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts
  16. 44
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  17. 23
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  18. 7
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  19. 7
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  20. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -21,6 +21,7 @@ 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,6 +51,10 @@ 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()}`, createDefaultHttpOptions(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));
}
@ -61,4 +66,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()}`, createDefaultHttpOptions({type}, config));
}
}

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,

30
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>
@ -52,12 +52,38 @@
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
@if (!data.entityId) {
<div class="flex flex-col gap-4" formGroupName="entityId">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.target-entity-type</mat-label>
<mat-select formControlName="entityType">
@for (type of alarmRuleEntityTypeList; track type) {
<mat-option [value]="type">{{ entityTypeTranslations.get(type).type | translate }}</mat-option>
}
</mat-select>
@if (fieldFormGroup.get('entityId.entityType').hasError('required')) {
<mat-error>{{ 'alarm-rule.entity-type-required' | translate }}</mat-error>
}
</mat-form-field>
@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"/>

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.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CUSTOMER];
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, ...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>

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

@ -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.
*/
@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-form-panel {
max-width: 100%;
width: 600px;
min-width: 100%;
}
.tb-alarm-filter-config-component {
flex: 1;
tb-entity-subtype-list {
flex: 1;
@media #{$mat-gt-xs} {
width: 180px;
}
.mdc-evolution-chip-set__chips {
width: 100%;
}
}
}
}

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

@ -0,0 +1,301 @@
///
/// 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,
OnDestroy,
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;
}
// @dynamic
@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, OnDestroy, 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.DEVICE_PROFILE, EntityType.ASSET_PROFILE, EntityType.CUSTOMER];
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);
}
}
ngOnDestroy(): void {
}
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 } 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: AlarmFilterConfig) {
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,

50
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';
@ -55,6 +56,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 +67,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField),
};
alarmRuleFilterConfig: any;
constructor(private calculatedFieldsService: CalculatedFieldsService,
private translate: TranslateService,
private dialog: MatDialog,
@ -77,11 +82,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 +125,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 +179,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 +220,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 +241,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();
}

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,
}
}
]
}
];

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);
}

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

@ -911,3 +911,10 @@ export function notEmptyObjectValidator(): ValidatorFn {
return null;
};
}
export interface CalculatedFieldsQuery {
type: CalculatedFieldType;
entityType?: EntityType;
entities?: Array<string>;
name?: string;
}

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

@ -1260,12 +1260,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.",
@ -1377,7 +1383,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