diff --git a/ui-ngx/src/app/core/http/calculated-fields.service.ts b/ui-ngx/src/app/core/http/calculated-fields.service.ts index 631550ae9e..0fba704277 100644 --- a/ui-ngx/src/app/core/http/calculated-fields.service.ts +++ b/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(`/api/calculatedField/${calculatedFieldId}`, defaultHttpOptionsFromConfig(config)); } + public getCalculatedFieldsFilter(pageLink: PageLink, query: CalculatedFieldsQuery, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams(query, config)); + } + public getCalculatedFields({ entityType, id }: EntityId, pageLink: PageLink, type?: CalculatedFieldType, config?: RequestConfig): Observable> { - return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, createDefaultHttpOptions(type ? {type} : null, config)); + return this.http.get>(`/api/${entityType}/${id}/calculatedFields${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type} , config)); } public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable { @@ -61,4 +71,8 @@ export class CalculatedFieldsService { public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable { return this.http.get(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config)); } + + public getAlarmRuleNames(pageLink: PageLink, type: CalculatedFieldType, config?: RequestConfig): Observable> { + return this.http.get>(`/api/calculatedFields/names${pageLink.toQuery()}`, defaultHttpOptionsFromParams({type}, config)); + } } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 5cd4167afa..4daae2115d 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/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; } diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 4277f96747..8909663771 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/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([ 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.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, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index e0f1a052d5..d4a93d461f 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+

{{ 'alarm-rule.alarm-rule' | translate}}

@@ -47,30 +47,53 @@
+ @if (!data.entityId) { +
+ + + @if (fieldFormGroup.get('entityId.entityType').value) { + + } +
+ }
{{ 'calculated-fields.arguments' | translate }}
-
{{ 'alarm-rule.create-alarm-rules' | translate }}
+
{{ 'alarm-rule.create-conditions' | translate }}
-
{{ 'alarm-rule.clear-alarm-rule' | translate }}
+
{{ 'alarm-rule.clear-condition' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index 0ea3bd036e..b3176414ab 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/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(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 + + + + + + + + + +
+ +
+ + + + +
+
+
+ +
+
+
alarm-rule.alarm-rule-type-list
+ + +
+
+
alarm-rule.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + + {{ entityTypeTranslations.get(type)?.type | translate }} + + + +
+ @if (alarmRuleFilterConfigForm.get('entityType').value) { +
+
alarm-rule.alarm-rule-entity-list
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.scss new file mode 100644 index 0000000000..12dd46a278 --- /dev/null +++ b/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%; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts new file mode 100644 index 0000000000..c961cebe33 --- /dev/null +++ b/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('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; + + @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); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html new file mode 100644 index 0000000000..807dd5c9f3 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.scss new file mode 100644 index 0000000000..ee0899b5ca --- /dev/null +++ b/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%; +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts new file mode 100644 index 0000000000..cf5462d1a2 --- /dev/null +++ b/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 { + + get alarmRuleTableConfig(): AlarmRulesTableConfig { + return this.entitiesTableConfig as AlarmRulesTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + alarmRuleFilterChanged(alarmRuleFilterConfig: CalculatedFieldsQuery) { + this.alarmRuleTableConfig.alarmRuleFilterConfig = alarmRuleFilterConfig; + this.alarmRuleTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts index 7acfca7b0d..c02c11a170 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 5167e32853..a51fd6e6d7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/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 { @@ -64,6 +68,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { 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 { 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 { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); - this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', '67%', + if (this.pageMode) { + this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.entity-type', '15%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('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('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('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 { } fetchCalculatedFields(pageLink: PageLink): Observable> { - 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 { }); } + 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 { return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, @@ -205,7 +242,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { 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, }, diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html index e9eebb226c..fb0eab1447 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.html @@ -16,5 +16,5 @@ --> @if (alarmRulesTableConfig) { - + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss index 0e0dacc392..0884f69200 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.scss +++ b/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; } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index 4ade1c5461..de0dc87c12 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/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(); } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html index d2f98ecd85..b3c0028f87 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html @@ -22,7 +22,7 @@
{{ 'alarm-rule.expression-type.simple' | translate }} - {{ 'alarm-rule.expression-type.tbel' | translate }} + {{ 'alarm-rule.expression-type.script' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts index 17ed11d892..2e8d116ad2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts +++ b/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; 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, - }, + } }; } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html index cd6b2c3796..14a88b2bf1 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html @@ -16,7 +16,7 @@ -->
- + @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts index 88ae1dd79d..f5e57310a2 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts +++ b/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(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) { diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss b/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss index 14df9a3972..b66573f417 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rules-dialog.component.scss +++ b/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; } } } } - diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html index 820418a6b0..b0e6164d30 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html @@ -55,6 +55,7 @@