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 af7d921d52..2941ee491d 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,44 @@ /> @if (!data.entityId) { - - +
+ + {{ 'entity.entity-type' | translate }} + + @for (type of alarmRuleEntityTypeList; track type) { + + {{ entityTypeTranslations.get(type)?.type | translate }} + + } + + + @if (isProfileEntityType) { + + + } @else { + + + } +
}
{{ 'calculated-fields.arguments' | translate }}
{ - @ViewChild('entitySelect') entitySelect!: EntitySelectComponent; - fieldFormGroup: FormGroup ; additionalDebugActionConfig = this.data.value?.id ? { @@ -97,11 +95,18 @@ 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 translate: TranslateService, private destroyRef: DestroyRef, private cfFormService: CalculatedFieldFormService) { super(store, router, dialogRef); @@ -120,6 +125,14 @@ 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)) @@ -127,7 +140,9 @@ export class AlarmRuleDialogComponent extends DialogComponent { - this.disabledArguments = !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) { argsControl.disable({ emitEvent: false }); @@ -174,24 +189,84 @@ 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; - - this.alarmRulesService.saveAlarmRule(alarmRule) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - next: calculatedField => this.dialogRef.close(calculatedField), - error: () => this.isLoading = false - }); + const value = this.fromGroupValue; + value.configuration.type = CalculatedFieldType.ALARM; + this.resolveEntityIds().pipe( + switchMap(entityIds => + entityIds.length + ? 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((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.entitySelect?.entityAutocompleteMarkAsTouched(); + this.fieldFormGroup.get('entityId')?.markAsTouched(); } } + 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 }))); + } + } + + private toProfileEntityIds(profiles: EntityInfoData[], names: string[]): EntityId[] { + const idByName = new Map(profiles.map(profile => [profile.name, profile.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 { @@ -230,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 9e95a55938..288d3dc91e 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 @@ -267,11 +267,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig { if (res) { this.updateData(); @@ -279,8 +278,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 c4d4552999..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 @@ -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(): EntityType { + return (Array.isArray(this.entityId) ? this.entityId[0]?.entityType : this.entityId?.entityType) as 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..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 @@ -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) { { - 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, - }; +export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entityId: EntityId | EntityId[]): EntityFilter => { + 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 }; + } + + 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) => { 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 1007fa3eea..631aef3eba 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1559,6 +1559,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",