From fe843614b984fdf24ad2fc7bbf1bb0396aeb4e48 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 26 May 2026 16:41:00 +0200 Subject: [PATCH 1/6] Added ability for creating alarm rules across multiple entities --- .../alarm-rule-dialog.component.html | 35 +++++++++++++------ .../alarm-rule-dialog.component.ts | 32 ++++++++++++----- ...lculated-field-argument-panel.component.ts | 12 ++++--- ...culated-field-arguments-table.component.ts | 2 +- .../entity/entity-list-select.component.html | 2 ++ .../entity/entity-list-select.component.ts | 10 +++++- .../shared/models/calculated-field.models.ts | 10 +++++- 7 files changed, 77 insertions(+), 26 deletions(-) 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 48f4e8a5af..de6b0ed243 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 @@ -56,23 +56,36 @@ /> @if (!data.entityId) { - - + @if (createNew) { + + + } @else { + + + } }
{{ 'calculated-fields.arguments' | translate }}
{ - @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; + @ViewChild('entitySelect') entitySelect!: EntitySelectComponent | EntityTypeListComponent; fieldFormGroup: FormGroup ; @@ -96,6 +97,7 @@ export class AlarmRuleDialogComponent extends DialogComponent, protected router: Router, @@ -120,6 +122,7 @@ export class AlarmRuleDialogComponent extends DialogComponent { - this.disabledArguments = !entityId || !name?.length; + this.disabledArguments = Array.isArray(entityId) ? !entityId.length : !entityId || !name?.length; const argsControl = this.fieldFormGroup.get('configuration.arguments')!; if (this.disabledArguments) { argsControl.disable({ emitEvent: false }); @@ -174,21 +177,34 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { this.isLoading = true; - const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; - alarmRule.configuration.type = CalculatedFieldType.ALARM; + const entityIds = Array.isArray(this.fieldFormGroup.value.entityId) + ? this.fieldFormGroup.value.entityId + : [this.fieldFormGroup.value.entityId ?? this.data.entityId]; - this.alarmRulesService.saveAlarmRule(alarmRule) + const requests$ = entityIds.map((entityId: EntityId) => { + const alarmRule = { ...(this.data.value ?? {}), ...this.fromGroupValue, entityId }; + alarmRule.configuration.type = CalculatedFieldType.ALARM; + return this.alarmRulesService.saveAlarmRule(alarmRule); + }); + + forkJoin(requests$) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ - next: calculatedField => this.dialogRef.close(calculatedField), + next: (calculatedFields: CalculatedFieldAlarmRule) => this.dialogRef.close(calculatedFields), error: () => this.isLoading = false }); } else { this.fieldFormGroup.get('name').markAsTouched(); - this.entitySelect?.entityAutocompleteMarkAsTouched(); + if (this.entitySelect instanceof EntitySelectComponent) { + this.entitySelect?.entityAutocompleteMarkAsTouched(); + } } } + get selectedEntityId(): EntityId { + return Array.isArray(this.fieldFormGroup.get('entityId').value) ? this.fieldFormGroup.get('entityId').value : this.fieldFormGroup.get('entityId').value; + } + private applyDialogData(): void { 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}); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index c4d4552999..38688594da 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -66,7 +66,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI @Input() buttonTitle: string; @Input() argument: CalculatedFieldArgumentValue; - @Input() entityId: EntityId; + @Input() entityId: EntityId | EntityId[]; @Input() tenantId: string; @Input() entityName: string; @Input() ownerId: EntityId; @@ -154,7 +154,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI get enableAttributeScopeSelection(): boolean { return this.entityType === ArgumentEntityType.Device || (this.entityType === ArgumentEntityType.Current - && (this.entityId.entityType === EntityType.DEVICE || this.entityId.entityType === EntityType.DEVICE_PROFILE)) + && (this.entityIdType === EntityType.DEVICE || this.entityIdType === EntityType.DEVICE_PROFILE)) } ngOnInit(): void { @@ -221,7 +221,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI private updatedArgumentType(): void { let argumentType = ArgumentEntityType.Current; if (this.argument.refDynamicSourceConfiguration?.type === ArgumentEntityType.Owner) { - this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE); + this.enableAutocomplete = (this.entityIdType === EntityType.DEVICE_PROFILE || this.entityIdType === EntityType.ASSET_PROFILE); argumentType = ArgumentEntityType.Owner; } else if (this.argument.refEntityId?.entityType) { argumentType = this.argument.refEntityId.entityType; @@ -294,7 +294,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(type => { this.argumentFormGroup.get('refEntityId').setValue(null); - this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner; + this.enableAutocomplete = (this.entityIdType === EntityType.DEVICE_PROFILE || this.entityIdType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner; this.updatedRefEntityIdState(type); if (!this.enableAttributeScopeSelection) { this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE); @@ -341,4 +341,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI this.entityNameSubject.next(null); } } + + get entityIdType() { + return Array.isArray(this.entityId) ? this.entityId[0].entityType : this.entityId.entityType; + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index 14cfde23fe..34f753d59c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -84,7 +84,7 @@ import { BaseData } from '@shared/models/base-data'; }) export class CalculatedFieldArgumentsTableComponent implements ControlValueAccessor, Validator, OnChanges, AfterViewInit { - @Input() entityId: EntityId; + @Input() entityId: EntityId | EntityId[]; @Input() tenantId: string; @Input() entityName: string; @Input() ownerId: EntityId; diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index 034f118b4d..8ddf26c8b1 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -21,6 +21,7 @@ [inlineField]="inlineField" [class.flex-1]="inlineField && !modelValue.entityType" style="min-width: 100px; padding-right: 8px;" + [label]="entityTypeLabel ?? null" [showLabel]="true" [required]="required" subscriptSizing="dynamic" @@ -35,6 +36,7 @@ @if (modelValue.entityType) { { }; diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 552d2a3a95..7791dfaf0f 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -33,6 +33,7 @@ import { EntitySearchDirection } from '@shared/models/relation.models'; import { AbstractControl, FormControl, ValidationErrors, ValidatorFn } from '@angular/forms'; import { AlarmRule } from "@shared/models/alarm-rule.models"; import { AlarmSeverity } from "@shared/models/alarm.models"; +import { EntityFilter } from '@shared/models/query/query.models'; export const FORBIDDEN_NAMES = ['ctx', 'e', 'pi']; @@ -527,7 +528,14 @@ export const ArgumentEntityTypeParamsMap =new Map { +export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entityId: EntityId | EntityId[]): EntityFilter => { + if (Array.isArray(entityId)) { + return { + type: AliasFilterType.entityList, + entityType: entityId[0].entityType as EntityType, + entityList: entityId.map(value => value.id) + } + } switch (entityId?.entityType) { case EntityType.ASSET_PROFILE: return { From 20be2a82169e39d395cbccc896178d15be2562a3 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 4 Jun 2026 15:12:03 +0200 Subject: [PATCH 2/6] Minor improvements --- .../alarm-rule-dialog.component.html | 43 +++++++--- .../alarm-rule-dialog.component.ts | 86 +++++++++++++------ .../shared/models/calculated-field.models.ts | 23 ++++- 3 files changed, 112 insertions(+), 40 deletions(-) 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 de6b0ed243..3cd708fd25 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 @@ -57,16 +57,37 @@
@if (!data.entityId) { @if (createNew) { - - +
+ + {{ 'entity.entity-type' | translate }} + + @for (type of alarmRuleEntityTypeList; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + + @if (isProfileType) { + + + } @else { + + + } +
} @else {
{{ 'calculated-fields.arguments' | translate }}
{ - @ViewChild('entitySelect') entitySelect!: EntitySelectComponent | EntityTypeListComponent; + @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; fieldFormGroup: FormGroup ; @@ -99,11 +101,17 @@ export class AlarmRuleDialogComponent extends DialogComponent(EntityType.DEVICE_PROFILE, { nonNullable: true, validators: Validators.required }); + + argsEntityId: EntityId | EntityId[]; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData, protected dialogRef: MatDialogRef, private alarmRulesService: AlarmRulesService, + private deviceProfileService: DeviceProfileService, + private assetProfileService: AssetProfileService, private destroyRef: DestroyRef, private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); @@ -123,6 +131,10 @@ export class AlarmRuleDialogComponent extends DialogComponent this.fieldFormGroup.get('entityId')!.reset(null)); + combineLatest([ this.fieldFormGroup.get('entityId')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('entityId')!.value)), this.fieldFormGroup.get('name')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('name')!.value)) @@ -131,6 +143,7 @@ export class AlarmRuleDialogComponent extends DialogComponent { this.disabledArguments = Array.isArray(entityId) ? !entityId.length : !entityId || !name?.length; + this.argsEntityId = Array.isArray(entityId) ? entityId.map(id => ({ entityType: this.entityTypeControl.value, id })) : entityId; const argsControl = this.fieldFormGroup.get('configuration.arguments')!; if (this.disabledArguments) { argsControl.disable({ emitEvent: false }); @@ -141,6 +154,15 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { this.isLoading = true; - const entityIds = Array.isArray(this.fieldFormGroup.value.entityId) - ? this.fieldFormGroup.value.entityId - : [this.fieldFormGroup.value.entityId ?? this.data.entityId]; - - const requests$ = entityIds.map((entityId: EntityId) => { - const alarmRule = { ...(this.data.value ?? {}), ...this.fromGroupValue, entityId }; - alarmRule.configuration.type = CalculatedFieldType.ALARM; - return this.alarmRulesService.saveAlarmRule(alarmRule); + this.resolveEntityIds().pipe( + switchMap(entityIds => + forkJoin(entityIds.map((entityId: EntityId) => { + const alarmRule = { ...(this.data.value ?? {}), ...this.fromGroupValue, entityId }; + alarmRule.configuration.type = CalculatedFieldType.ALARM; + return this.alarmRulesService.saveAlarmRule(alarmRule); + }))), + takeUntilDestroyed(this.destroyRef) + ).subscribe({ + next: (calculatedFields: CalculatedFieldAlarmRule[]) => this.dialogRef.close(calculatedFields[0]), + error: () => this.isLoading = false }); - - forkJoin(requests$) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - next: (calculatedFields: CalculatedFieldAlarmRule) => this.dialogRef.close(calculatedFields), - error: () => this.isLoading = false - }); } else { this.fieldFormGroup.get('name').markAsTouched(); - if (this.entitySelect instanceof EntitySelectComponent) { - this.entitySelect?.entityAutocompleteMarkAsTouched(); - } + this.entitySelect?.entityAutocompleteMarkAsTouched(); + } + } + + private resolveEntityIds(): Observable { + const value = this.fieldFormGroup.get('entityId').value; + if (!Array.isArray(value)) { + return of([value ?? this.data.entityId]); + } + switch (this.entityTypeControl.value) { + case EntityType.DEVICE_PROFILE: + return this.deviceProfileService.getDeviceProfileNames(false).pipe( + map(profiles => this.toProfileEntityIds(profiles, value)) + ); + case EntityType.ASSET_PROFILE: + return this.assetProfileService.getAssetProfileNames(false).pipe( + map(profiles => this.toProfileEntityIds(profiles, value)) + ); + default: + return of(value.map(id => ({ entityType: this.entityTypeControl.value, id }))); } } - get selectedEntityId(): EntityId { - return Array.isArray(this.fieldFormGroup.get('entityId').value) ? this.fieldFormGroup.get('entityId').value : this.fieldFormGroup.get('entityId').value; + private toProfileEntityIds(profiles: EntityInfoData[], names: string[]): EntityId[] { + const idByName = new Map(profiles.map(profile => [profile.name, profile.id])); + return names.map(name => idByName.get(name)).filter((id): id is EntityId => !!id); } private applyDialogData(): void { diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 7791dfaf0f..aa3afe1bb3 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -530,10 +530,25 @@ export const ArgumentEntityTypeParamsMap =new Map { if (Array.isArray(entityId)) { - return { - type: AliasFilterType.entityList, - entityType: entityId[0].entityType as EntityType, - entityList: entityId.map(value => value.id) + const entityType = entityId[0]?.entityType as EntityType; + const ids = entityId.map(value => value.id); + switch (entityType) { + case EntityType.ASSET_PROFILE: + return { + type: AliasFilterType.assetType, + assetTypes: ids + }; + case EntityType.DEVICE_PROFILE: + return { + type: AliasFilterType.deviceType, + deviceTypes: ids + }; + default: + return { + type: AliasFilterType.entityList, + entityType, + entityList: ids + }; } } switch (entityId?.entityType) { From 0374b78efa63cd7d678834b07326abd293dc6be7 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 4 Jun 2026 16:00:30 +0200 Subject: [PATCH 3/6] Fixed Advanced Widget Style Editor rendering --- .../modules/home/components/widget/widget-config.component.html | 2 +- .../modules/home/components/widget/widget-config.component.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index e194b48ae9..25896c39cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -36,7 +36,7 @@ } -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss index af7e27f481..212bc0724a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss @@ -43,7 +43,6 @@ } .tb-widget-config-content { & > .mat-content { - height: 100%; padding-top: 8px; @media #{$mat-xs} { padding-left: 8px; From a8b829fdd917abbfb3ce2d13af351e85873ea895 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 15 Jun 2026 12:17:00 +0200 Subject: [PATCH 4/6] Fixes --- .../alarm-rule-dialog.component.html | 15 ++++-- .../alarm-rule-dialog.component.ts | 29 +++++------ ...lculated-field-argument-panel.component.ts | 2 +- .../entity/entity-list-select.component.html | 2 +- .../entity/entity-list-select.component.ts | 8 ++- .../shared/models/calculated-field.models.ts | 50 +++++-------------- 6 files changed, 42 insertions(+), 64 deletions(-) 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 3cd708fd25..4132b2fd97 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 @@ -68,14 +68,23 @@ } - @if (isProfileType) { + @if (entityTypeControl.value === EntityType.DEVICE_PROFILE) { + [entityType]="EntityType.DEVICE" + [label]="entityTypeTranslations.get(EntityType.DEVICE_PROFILE)?.typePlural | translate"> + + } @else if (entityTypeControl.value === EntityType.ASSET_PROFILE) { + } @else { - forkJoin(entityIds.map((entityId: EntityId) => { - const alarmRule = { ...(this.data.value ?? {}), ...this.fromGroupValue, entityId }; - alarmRule.configuration.type = CalculatedFieldType.ALARM; - return this.alarmRulesService.saveAlarmRule(alarmRule); - }))), + entityIds.length + ? forkJoin(entityIds.map((entityId: EntityId) => { + const alarmRule = { ...(this.data.value ?? {}), ...this.fromGroupValue, entityId }; + alarmRule.configuration.type = CalculatedFieldType.ALARM; + return this.alarmRulesService.saveAlarmRule(alarmRule); + })) + : of([] as CalculatedFieldAlarmRule[])), takeUntilDestroyed(this.destroyRef) ).subscribe({ - next: (calculatedFields: CalculatedFieldAlarmRule[]) => this.dialogRef.close(calculatedFields[0]), + next: (calculatedFields: CalculatedFieldAlarmRule[]) => + calculatedFields.length ? this.dialogRef.close(calculatedFields[0]) : this.isLoading = false, error: () => this.isLoading = false }); } else { this.fieldFormGroup.get('name').markAsTouched(); + this.fieldFormGroup.get('entityId')?.markAsTouched(); this.entitySelect?.entityAutocompleteMarkAsTouched(); } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index 38688594da..dcc7f57154 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -343,6 +343,6 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } get entityIdType() { - return Array.isArray(this.entityId) ? this.entityId[0].entityType : this.entityId.entityType; + return Array.isArray(this.entityId) ? this.entityId[0]?.entityType : this.entityId?.entityType; } } diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index 8ddf26c8b1..ae719429f0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -36,7 +36,7 @@ @if (modelValue.entityType) { { }; diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index aa3afe1bb3..eff0810af5 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -529,45 +529,21 @@ export const ArgumentEntityTypeParamsMap =new Map { - if (Array.isArray(entityId)) { - const entityType = entityId[0]?.entityType as EntityType; - const ids = entityId.map(value => value.id); - switch (entityType) { - case EntityType.ASSET_PROFILE: - return { - type: AliasFilterType.assetType, - assetTypes: ids - }; - case EntityType.DEVICE_PROFILE: - return { - type: AliasFilterType.deviceType, - deviceTypes: ids - }; - default: - return { - type: AliasFilterType.entityList, - entityType, - entityList: ids - }; - } + const entityType = (Array.isArray(entityId) ? entityId[0]?.entityType : entityId?.entityType) as EntityType; + + if (entityType === EntityType.DEVICE_PROFILE || entityType === EntityType.ASSET_PROFILE) { + const profileNames = Array.isArray(entityId) ? entityId.map(value => value.id) : [entityName]; + return entityType === EntityType.ASSET_PROFILE + ? { type: AliasFilterType.assetType, assetTypes: profileNames } + : { type: AliasFilterType.deviceType, deviceTypes: profileNames }; } - switch (entityId?.entityType) { - case EntityType.ASSET_PROFILE: - return { - assetTypes: [entityName], - type: AliasFilterType.assetType - }; - case EntityType.DEVICE_PROFILE: - return { - deviceTypes: [entityName], - type: AliasFilterType.deviceType - }; - default: - return { - type: AliasFilterType.singleEntity, - singleEntity: entityId, - }; + + if (Array.isArray(entityId)) { + return entityId.length === 1 + ? { type: AliasFilterType.singleEntity, singleEntity: entityId[0] } + : { type: AliasFilterType.entityList, entityType, entityList: entityId.map(value => value.id) }; } + return { type: AliasFilterType.singleEntity, singleEntity: entityId }; } export const debugCfActionEnabled = (cf: CalculatedField) => { From 7190de47fb49afaabf18434b8f2f9ddaa3027a12 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 9 Jul 2026 10:47:19 +0200 Subject: [PATCH 5/6] Minor fixes --- .../alarm-rule-dialog.component.html | 80 ++++++----------- .../alarm-rule-dialog.component.ts | 89 ++++++++++++------- .../alarm-rules/alarm-rules-table-config.ts | 8 +- ...lculated-field-argument-panel.component.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 3 + 5 files changed, 95 insertions(+), 89 deletions(-) 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 4132b2fd97..bacb70a431 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 @@ -56,59 +56,37 @@ />
@if (!data.entityId) { - @if (createNew) { -
- - {{ 'entity.entity-type' | translate }} - - @for (type of alarmRuleEntityTypeList; track type) { - - {{ entityTypeTranslations.get(type)?.type | translate }} - - } - - - @if (entityTypeControl.value === EntityType.DEVICE_PROFILE) { - - - } @else if (entityTypeControl.value === EntityType.ASSET_PROFILE) { - - - } @else { - - - } -
- } @else { - + + {{ 'entity.entity-type' | translate }} + + @for (type of alarmRuleEntityTypeList; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + + @if (isProfileEntityType) { + + + } @else { + - - } + subscriptSizing="dynamic" + [entityType]="entityTypeControl.value" + labelText="{{ entityTypeTranslations.get(entityTypeControl.value)?.typePlural | 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 c0c8a57ee5..41e226e240 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 @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -41,19 +41,16 @@ import { AlarmRuleTestScriptFn } from "@shared/models/alarm-rule.models"; import { deepTrim } from "@core/utils"; -import { combineLatest, forkJoin, Observable, of } from 'rxjs'; -import { debounceTime, map, startWith, switchMap } from 'rxjs/operators'; +import { combineLatest, from, Observable, of } from 'rxjs'; +import { catchError, debounceTime, map, mergeMap, startWith, switchMap, toArray } from 'rxjs/operators'; import { RelationTypes } from "@shared/models/relation.models"; import { StringItemsOption } from "@shared/components/string-items-list.component"; -import { BaseData } from "@shared/models/base-data"; import { CalculatedFieldFormService } from '@core/services/calculated-field-form.service'; -import { EntitySelectComponent } from '@shared/components/entity/entity-select.component'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; -import { AssetInfo } from '@shared/models/asset.models'; -import { DeviceInfo } from '@shared/models/device.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { AssetProfileService } from '@core/http/asset-profile.service'; import { EntityInfoData } from '@shared/models/entity.models'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; export interface AlarmRuleDialogData { value?: CalculatedFieldAlarmRule; @@ -76,8 +73,6 @@ export interface AlarmRuleDialogData { }) export class AlarmRuleDialogComponent extends DialogComponent { - @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; - fieldFormGroup: FormGroup ; additionalDebugActionConfig = this.data.value?.id ? { @@ -99,7 +94,6 @@ export class AlarmRuleDialogComponent extends DialogComponent(EntityType.DEVICE_PROFILE, { nonNullable: true, validators: Validators.required }); @@ -112,6 +106,7 @@ export class AlarmRuleDialogComponent extends DialogComponent this.fieldFormGroup.get('entityId')!.reset(null)); @@ -142,7 +140,8 @@ export class AlarmRuleDialogComponent extends DialogComponent { - this.disabledArguments = Array.isArray(entityId) ? !entityId.length : !entityId || !name?.length; + const hasEntity = Array.isArray(entityId) ? !!entityId.length : !!entityId; + this.disabledArguments = !hasEntity || !name?.length; this.argsEntityId = Array.isArray(entityId) ? entityId.map(id => ({ entityType: this.entityTypeControl.value, id })) : entityId; const argsControl = this.fieldFormGroup.get('configuration.arguments')!; if (this.disabledArguments) { @@ -190,25 +189,38 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { this.isLoading = true; + const value = this.fromGroupValue; + value.configuration.type = CalculatedFieldType.ALARM; this.resolveEntityIds().pipe( switchMap(entityIds => entityIds.length - ? forkJoin(entityIds.map((entityId: EntityId) => { - const alarmRule = { ...(this.data.value ?? {}), ...this.fromGroupValue, entityId }; - alarmRule.configuration.type = CalculatedFieldType.ALARM; - return this.alarmRulesService.saveAlarmRule(alarmRule); - })) + ? from(entityIds).pipe( + mergeMap(entityId => + this.alarmRulesService.saveAlarmRule({ ...(this.data.value ?? {}), ...value, entityId }).pipe( + catchError(() => of(null as CalculatedFieldAlarmRule)) + ), 4), + toArray() + ) : of([] as CalculatedFieldAlarmRule[])), takeUntilDestroyed(this.destroyRef) - ).subscribe({ - next: (calculatedFields: CalculatedFieldAlarmRule[]) => - calculatedFields.length ? this.dialogRef.close(calculatedFields[0]) : this.isLoading = false, - error: () => this.isLoading = false + ).subscribe((results: CalculatedFieldAlarmRule[]) => { + const saved = results.filter(Boolean); + const failedCount = results.length - saved.length; + if (failedCount) { + this.showNotification( + this.translate.instant('alarm-rule.alarm-rules-creation-failed', { count: failedCount, total: results.length }), 'error'); + } else if (saved.length > 1) { + this.showNotification(this.translate.instant('alarm-rule.alarm-rules-created', { count: saved.length }), 'success'); + } + if (saved.length) { + this.dialogRef.close(saved[0]); + } else { + this.isLoading = false; + } }); } else { this.fieldFormGroup.get('name').markAsTouched(); this.fieldFormGroup.get('entityId')?.markAsTouched(); - this.entitySelect?.entityAutocompleteMarkAsTouched(); } } @@ -233,12 +245,28 @@ export class AlarmRuleDialogComponent extends DialogComponent [profile.name, profile.id])); - return names.map(name => idByName.get(name)).filter((id): id is EntityId => !!id); + const notFoundNames = names.filter(name => !idByName.has(name)); + if (notFoundNames.length) { + this.showNotification( + this.translate.instant('alarm-rule.profiles-not-found', { names: notFoundNames.join(', ') }), 'error'); + return []; + } + return names.map(name => idByName.get(name)); + } + + private showNotification(message: string, type: 'success' | 'error'): void { + this.store.dispatch(new ActionNotificationShow({ + message, + type, + verticalPosition: 'top', + horizontalPosition: 'left', + duration: 5000 + })); } private applyDialogData(): void { 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}); + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId: this.data.entityId ? entityId : null, ...value }, {emitEvent: false}); } onTestScript(expression: string): Observable { @@ -277,14 +305,11 @@ export class AlarmRuleDialogComponent extends DialogComponent): void { - this.entityName = entity.name; - if (this.isAssignedToCustomer(entity as AssetInfo | DeviceInfo)) { - this.ownerId = (entity as AssetInfo | DeviceInfo).customerId; - } + get isProfileEntityType(): boolean { + return this.entityTypeControl.value === EntityType.DEVICE_PROFILE || this.entityTypeControl.value === EntityType.ASSET_PROFILE; } - private isAssignedToCustomer(entity: AssetInfo | DeviceInfo): boolean { - return entity && entity.customerId && entity.customerId.id !== NULL_UUID; + get subtypeListEntityType(): EntityType { + return this.entityTypeControl.value === EntityType.DEVICE_PROFILE ? EntityType.DEVICE : EntityType.ASSET; } } 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 f5043bdcee..9877610439 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 @@ -258,11 +258,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig { if (res) { this.updateData(); @@ -270,8 +269,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig { - const entityId = this.entityId || value?.entityId; + private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false, + selectNewEntity = false): Observable { + const entityId = selectNewEntity ? null : (this.entityId || value?.entityId); const entityName = this.entityName || (value as CalculatedFieldAlarmRuleInfo)?.entityName; return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts index dcc7f57154..ec3b00b82c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts @@ -342,7 +342,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI } } - get entityIdType() { - return Array.isArray(this.entityId) ? this.entityId[0]?.entityType : this.entityId?.entityType; + get entityIdType(): EntityType { + return (Array.isArray(this.entityId) ? this.entityId[0]?.entityType : this.entityId?.entityType) as EntityType; } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 764676dfbc..2df57f9739 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1557,6 +1557,9 @@ "min-value": "Value must be 1 or greater.", "argument-in-use": "Argument is used as general argument.", "import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule structure.", + "alarm-rules-created": "{ count, plural, =1 {Alarm rule has been created} other {# alarm rules have been created} }", + "alarm-rules-creation-failed": "Failed to save {{count}} of {{total}} alarm rules.", + "profiles-not-found": "Unable to save alarm rules. Matching profiles not found: {{names}}", "no-filter-preview": "No filter specified", "filter-operation": { "and": "And", From 4ad993f084413a74e4f1cb8dbb0cd513977fd056 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 9 Jul 2026 12:50:17 +0200 Subject: [PATCH 6/6] Clean up --- .../modules/home/components/widget/widget-config.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index d5cee72b46..6285268abf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -36,7 +36,7 @@ } -
+