Browse Source

Merge 4ad993f084 into d4329f3b7c

pull/15694/merge
Maksym Tsymbarov 5 days ago
committed by GitHub
parent
commit
cadcc7a5c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 43
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  2. 132
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  3. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  4. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  5. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  6. 2
      ui-ngx/src/app/shared/components/entity/entity-list-select.component.html
  7. 6
      ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts
  8. 33
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  9. 3
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -56,23 +56,44 @@
/>
</div>
@if (!data.entityId) {
<tb-entity-select formControlName="entityId"
#entitySelect
appearance="outline"
required
entityTypeLabel="{{ 'entity.entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="alarmRuleEntityTypeList"
[defaultEntityType]="EntityType.DEVICE_PROFILE"
(entityChanged)="changeEntity($event)">
</tb-entity-select>
<div class="flex gap-1">
<mat-form-field class="w-1/4 h-fit" appearance="outline" subscriptSizing="dynamic">
<mat-label>{{ 'entity.entity-type' | translate }}</mat-label>
<mat-select [formControl]="entityTypeControl" required>
@for (type of alarmRuleEntityTypeList; track type) {
<mat-option [value]="type">
{{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (isProfileEntityType) {
<tb-entity-subtype-list formControlName="entityId"
class="flex-1"
appearance="outline"
required
subscriptSizing="dynamic"
[entityType]="subtypeListEntityType"
[label]="entityTypeTranslations.get(entityTypeControl.value)?.typePlural | translate">
</tb-entity-subtype-list>
} @else {
<tb-entity-list formControlName="entityId"
class="flex-1"
appearance="outline"
required
subscriptSizing="dynamic"
[entityType]="entityTypeControl.value"
labelText="{{ entityTypeTranslations.get(entityTypeControl.value)?.typePlural | translate }}">
</tb-entity-list>
}
</div>
}
</div>
<ng-container formGroupName="configuration">
<div class="tb-form-panel" [class.disabled]="disabledArguments">
<div class="tb-form-panel-title tb-required">{{ 'calculated-fields.arguments' | translate }}</div>
<tb-calculated-field-arguments-table formControlName="arguments"
[entityId]="data.entityId || fieldFormGroup.get('entityId').value"
[entityId]="data.entityId || argsEntityId"
[tenantId]="data.tenantId"
[ownerId]="ownerId"
[watchKeyChange]="true"

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

@ -14,11 +14,11 @@
/// 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';
import { FormGroup } from '@angular/forms';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component';
import {
@ -41,16 +41,16 @@ import {
AlarmRuleTestScriptFn
} from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils";
import { combineLatest, Observable } from "rxjs";
import { debounceTime, startWith } 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;
@ -73,8 +73,6 @@ export interface AlarmRuleDialogData {
})
export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogComponent, CalculatedFieldAlarmRule> {
@ViewChild('entitySelect') entitySelect!: EntitySelectComponent;
fieldFormGroup: FormGroup ;
additionalDebugActionConfig = this.data.value?.id ? {
@ -97,11 +95,18 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
disabledArguments = false;
isLoading = false;
entityTypeControl = new FormControl<EntityType>(EntityType.DEVICE_PROFILE, { nonNullable: true, validators: Validators.required });
argsEntityId: EntityId | EntityId[];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AlarmRuleDialogData,
protected dialogRef: MatDialogRef<AlarmRuleDialogComponent, CalculatedFieldAlarmRule>,
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<AlarmRuleDialogCom
});
if (!this.data.entityId) {
const copiedEntityType = this.data.value?.entityId?.entityType as EntityType;
if (alarmRuleEntityTypeList.includes(copiedEntityType)) {
this.entityTypeControl.setValue(copiedEntityType, { emitEvent: false });
}
this.entityTypeControl.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(() => 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<AlarmRuleDialogCom
debounceTime(50),
takeUntilDestroyed()
).subscribe(([entityId, name]) => {
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<AlarmRuleDialogCom
add(): void {
if (this.fieldFormGroup.valid && Object.keys(this.arguments ?? {}).length > 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<EntityId[]> {
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<string> {
@ -230,14 +305,11 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}));
}
changeEntity(entity: BaseData<EntityId>): 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;
}
}

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

@ -267,11 +267,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
$event?.stopPropagation();
const copyCalculatedAlarmRule = deepClone(calculatedField);
if (this.pageMode) {
copyCalculatedAlarmRule.entityId = null;
delete (copyCalculatedAlarmRule as CalculatedFieldAlarmRuleInfo).entityName;
}
delete copyCalculatedAlarmRule.id;
this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty)
this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty, this.pageMode)
.subscribe((res) => {
if (res) {
this.updateData();
@ -279,8 +278,9 @@ export class AlarmRulesTableConfig extends EntityTableConfig<AlarmRuleTableEntit
});
}
private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false): Observable<CalculatedFieldAlarmRule> {
const entityId = this.entityId || value?.entityId;
private getCalculatedAlarmDialog(value?: AlarmRuleTableEntity, buttonTitle = 'action.add', isDirty = false,
selectNewEntity = false): Observable<CalculatedFieldAlarmRule> {
const entityId = selectNewEntity ? null : (this.entityId || value?.entityId);
const entityName = this.entityName || (value as CalculatedFieldAlarmRuleInfo)?.entityName;
return this.dialog.open<AlarmRuleDialogComponent, AlarmRuleDialogData, CalculatedFieldAlarmRule>(AlarmRuleDialogComponent, {
disableClose: true,

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

2
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;

2
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) {
<tb-entity-list
class="flex-1"
[labelText]="entityListLabel ?? ''"
[inlineField]="inlineField"
[class.tb-not-empty]="modelValue.ids?.length > 0"
[required]="required"

6
ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts

@ -76,6 +76,12 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit {
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
entityTypeLabel: string;
@Input()
entityListLabel: string;
displayEntityTypeSelect: boolean;
private defaultEntityType: EntityType | AliasEntityType = null;

33
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,24 +528,22 @@ export const ArgumentEntityTypeParamsMap =new Map<ArgumentEntityType, ArgumentEn
[ArgumentEntityType.Customer, { title: 'calculated-fields.customer-name', entityType: EntityType.CUSTOMER }],
])
export const getCalculatedFieldCurrentEntityFilter = (entityName: string, entityId: EntityId) => {
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) => {

3
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",

Loading…
Cancel
Save