Browse Source

Merge pull request #14423 from ArtemDzhereleiko/AD/bug-fixes/cf-alarm-rule

Fixed Calculated fields
pull/14428/head
Vladyslav Prykhodko 10 months ago
committed by GitHub
parent
commit
80548635a4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  2. 25
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  3. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  4. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts
  5. 81
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  6. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  7. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  8. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  9. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  10. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  11. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
  12. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  13. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  14. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  15. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  16. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  17. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  18. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  19. 25
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  20. 34
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  21. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  22. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  23. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  24. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  25. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  26. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  27. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  28. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  29. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  30. 46
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html
  31. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts
  32. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  33. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  34. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  35. 4
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  36. 8
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts
  37. 4
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  38. 8
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts
  39. 2
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html
  40. 7
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts
  41. 2
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  42. 8
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  43. 9
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  44. 38
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -58,6 +58,7 @@
<tb-entity-type-select appearance="outline"
subscriptSizing="dynamic"
showLabel
required
label="{{ 'alarm-rule.target-entity-type' | translate }}"
[filterAllowedEntityTypes]="false"
[allowedEntityTypes]="alarmRuleEntityTypeList"
@ -83,12 +84,14 @@
[entityId]="data.entityId || fieldFormGroup.get('entityId').value"
[tenantId]="data.tenantId"
[ownerId]="data.ownerId"
[watchKeyChange]="true"
[disabledAddButton]="!fieldFormGroup.get('entityId.id').value"
[entityName]="data.entityName"/>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ 'alarm-rule.create-conditions' | translate }}</div>
<div class="flex flex-1 flex-col">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments">
<tb-create-cf-alarm-rules formControlName="createRules" [arguments]="arguments" [testScript]="onTestScript.bind(this)">
</tb-create-cf-alarm-rules>
</div>
</div>
@ -97,7 +100,7 @@
<div class="flex flex-row items-center justify-start gap-2 pb-2"
[class.!hidden]="!configFormGroup.get('clearRule').value">
<div class="clear-alarm-rule flex flex-1 flex-row">
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments">
<tb-cf-alarm-rule formControlName="clearRule" class="flex-1" [arguments]="arguments" [testScript]="onTestScript.bind(this)" isClearCondition>
</tb-cf-alarm-rule>
</div>
<button mat-icon-button
@ -116,10 +119,7 @@
<div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'alarm-rule.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
(click)="addClearAlarmRule()">
{{ 'alarm-rule.add-clear-alarm-rule' | translate }}
</button>
</div>

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

@ -31,8 +31,15 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model';
import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips";
import { AlarmRule, AlarmRuleConditionType, AlarmRuleExpressionType } from "@shared/models/alarm-rule.models";
import {
AlarmRule,
AlarmRuleConditionType,
AlarmRuleExpressionType,
AlarmRuleTestScriptFn
} from "@shared/models/alarm-rule.models";
import { deepTrim } from "@core/utils";
import { Observable } from "rxjs";
import { switchMap } from "rxjs/operators";
export interface AlarmRuleDialogData {
value?: CalculatedField;
@ -43,6 +50,7 @@ export interface AlarmRuleDialogData {
ownerId: EntityId;
additionalDebugActionConfig: AdditionalDebugActionConfig<(calculatedField: CalculatedField) => void>;
isDirty?: boolean;
getTestScriptDialogFn: AlarmRuleTestScriptFn,
}
@Component({
@ -182,4 +190,19 @@ export class AlarmRuleDialogComponent extends DialogComponent<AlarmRuleDialogCom
}
});
}
onTestScript(expression: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, args, false);
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, expression, null, false);
}
}

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

@ -90,7 +90,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces
panelMode = false;
buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter');
buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
alarmRuleFilterConfigForm: FormGroup;
@ -281,6 +281,9 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces
if (this.alarmRuleFilterConfig?.name?.length) {
filterTextParts.push(this.alarmRuleFilterConfig.name.map((type) => this.customTranslate(type)).join(', '));
}
if (this.alarmRuleFilterConfig?.entityType) {
filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.alarmRuleFilterConfig.entityType).type));
}
if (!filterTextParts.length) {
this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title');
} else {

1
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts

@ -18,7 +18,6 @@ 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";

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

@ -26,7 +26,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store';
@ -36,13 +36,17 @@ import { DestroyRef, Renderer2 } from '@angular/core';
import { EntityDebugSettings } from '@shared/models/entity.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { catchError, filter, switchMap, tap } from 'rxjs/operators';
import {
ArgumentEntityType,
ArgumentType,
CalculatedField,
CalculatedFieldAlarmRule,
CalculatedFieldEventArguments,
CalculatedFieldsQuery,
CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights,
} from '@shared/models/calculated-field.models';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
@ -57,8 +61,13 @@ 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 { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils";
import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component";
import { ActionNotificationShow } from "@core/notification/notification.actions";
import {
CalculatedFieldScriptTestDialogComponent,
CalculatedFieldTestScriptDialogData
} from "@home/components/calculated-fields/components/test-dialog/calculated-field-script-test-dialog.component";
export class AlarmRulesTableConfig extends EntityTableConfig<any> {
@ -223,7 +232,10 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void {
const copyCalculatedAlarmRule = deepClone(calculatedField);
copyCalculatedAlarmRule.entityId = null;
if (this.pageMode) {
copyCalculatedAlarmRule.entityId = null;
}
delete copyCalculatedAlarmRule.id;
this.getCalculatedAlarmDialog(copyCalculatedAlarmRule, 'action.apply', isDirty)
.subscribe((res) => {
if (res) {
@ -245,6 +257,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId},
additionalDebugActionConfig: this.additionalDebugActionConfig,
isDirty,
getTestScriptDialogFn: this.getTestScriptDialog.bind(this),
},
enterAnimationDuration: isDirty ? 0 : null,
})
@ -277,6 +290,19 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
filter(Boolean),
switchMap(calculatedField => {
if (calculatedField.type !== CalculatedFieldType.ALARM) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('alarm-rule.import-invalid-alarm-rule-type'),
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left',
duration: 5000
}));
return EMPTY;
}
return of(calculatedField);
}),
switchMap(calculatedField => this.getCalculatedAlarmDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
@ -287,15 +313,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
}
private updateImportedCalculatedField(calculatedField: CalculatedField): CalculatedField {
if (calculatedField.type === CalculatedFieldType.GEOFENCING) {
calculatedField.configuration.zoneGroups = Object.keys(calculatedField.configuration.zoneGroups).reduce((acc, key) => {
const arg = calculatedField.configuration.zoneGroups[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
? { ...arg, refEntityId: { id: this.tenantId, entityType: ArgumentEntityType.Tenant } }
: arg;
return acc;
}, {});
} else {
if (calculatedField.type === CalculatedFieldType.ALARM) {
calculatedField.configuration.arguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const arg = calculatedField.configuration.arguments[key];
acc[key] = arg.refEntityId?.entityType === ArgumentEntityType.Tenant
@ -315,4 +333,41 @@ export class AlarmRulesTableConfig extends EntityTableConfig<any> {
takeUntilDestroyed(this.destroyRef),
).subscribe(() => this.updateData());
}
private getTestScriptDialog(calculatedField: CalculatedField, expression: string, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> {
if (calculatedField.type === CalculatedFieldType.ALARM) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
const type = calculatedField.configuration.arguments[key].refEntityKey.type;
acc[key] = isObject(argumentsObj) && argumentsObj.hasOwnProperty(key)
? {...argumentsObj[key], type}
: type === ArgumentType.Rolling ? {values: [], type} : {value: '', type, ts: new Date().getTime()};
return acc;
}, {});
return this.dialog.open<CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestScriptDialogData, string>(CalculatedFieldScriptTestDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit
}
}).afterClosed()
.pipe(
filter(Boolean),
tap(expression => {
if (openCalculatedFieldEdit) {
this.editCalculatedField({
entityId: this.entityId, ...calculatedField,
configuration: {...calculatedField.configuration, expression} as any
}, true)
}
}),
);
} else {
return of(null);
}
}
}

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

@ -67,9 +67,26 @@
[helpPopupStyle]="{ width: '1200px' }"
helpId="alarm-rule/expression_fn">
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.expression-type.script' | translate }}
class="tb-primary-background tbel-script-lang-chip">{{ 'alarm-rule.tbel' | translate }}
</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!argumentsList.length"
(click)="onTestScript()">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript()"
[disabled]="!argumentsList.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</div>
}
</div>

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

@ -28,6 +28,7 @@ import {
AlarmRuleCondition,
AlarmRuleConditionType,
AlarmRuleConditionTypeTranslationMap,
alarmRuleDefaultScript,
AlarmRuleExpressionType
} from "@shared/models/alarm-rule.models";
import {
@ -38,11 +39,13 @@ import {
import { TbEditorCompleter } from "@shared/models/ace/completion.models";
import { AceHighlightRules } from "@shared/models/ace/ace.models";
import { ComplexOperation, complexOperationTranslationMap } from "@shared/models/query/query.models";
import { Observable } from "rxjs";
export interface CfAlarmRuleConditionDialogData {
readonly: boolean;
condition: AlarmRuleCondition;
arguments?: Record<string, CalculatedFieldArgument>;
testScript: (expression: string) => Observable<string>;
}
@Component({
@ -117,7 +120,7 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.conditionFormGroup.patchValue({
expression: {
type: this.condition?.expression?.type ?? AlarmRuleExpressionType.SIMPLE,
expression: this.condition?.expression?.expression ?? null,
expression: this.condition?.expression?.expression ?? alarmRuleDefaultScript,
filters: this.condition?.expression?.filters ?? [],
operation: this.condition?.expression?.operation ?? ComplexOperation.AND
},
@ -223,4 +226,11 @@ export class CfAlarmRuleConditionDialogComponent extends DialogComponent<CfAlarm
this.dialogRef.close(this.conditionFormGroup.value as AlarmRuleCondition);
}
onTestScript() {
this.data.testScript(this.conditionFormGroup.get('expression.expression').value).subscribe(
(expression) => {
this.conditionFormGroup.get('expression.expression').setValue(expression);
this.conditionFormGroup.get('expression.expression').markAsDirty();
})
}
}

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html

@ -30,7 +30,7 @@
[nowrap]="true"
[specText]="specText"
required
addFilterPrompt="{{'alarm-rule.enter-alarm-rule-condition-prompt' | translate}}">
addFilterPrompt="{{ (isClearCondition ? 'alarm-rule.enter-alarm-rule-clear-condition-prompt' :'alarm-rule.enter-alarm-rule-condition-prompt') | translate }}">
</tb-alarm-rule-filter-text>
<mat-icon [color]="conditionSet() ? 'primary' : 'warn'" class="tb-mat-20 tb-alarm-rule-schedule-edit-icon">{{ conditionSet() ? 'edit' : 'add' }}</mat-icon>
</div>

20
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts

@ -21,8 +21,7 @@ import {
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
Validator,
Validators
Validator
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { deepClone, isDefinedAndNotNull } from '@core/utils';
@ -50,6 +49,7 @@ import {
CfAlarmScheduleDialogComponent
} from "@home/components/alarm-rules/cf-alarm-schedule-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({
selector: 'tb-cf-alarm-rule-condition',
@ -81,6 +81,13 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
isClearCondition = false;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
alarmRuleConditionFormGroup = this.fb.group({
type: ['SIMPLE'],
expression: [{type: AlarmRuleExpressionType.SIMPLE}],
@ -118,10 +125,8 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
}
writeValue(value: AlarmRuleCondition): void {
if (value) {
this.modelValue = value;
this.updateConditionInfo();
}
this.modelValue = value;
this.updateConditionInfo();
}
public conditionSet() {
@ -147,7 +152,8 @@ export class CfAlarmRuleConditionComponent implements ControlValueAccessor, Vali
data: {
readonly: this.disabled,
condition: this.disabled ? this.modelValue : deepClone(this.modelValue),
arguments: this.arguments
arguments: this.arguments,
testScript: this.testScript
}
}).afterClosed().subscribe((result) => {
if (result) {

2
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html

@ -16,7 +16,7 @@
-->
<div class="tb-form-panel no-border no-padding" [formGroup]="alarmRuleFormGroup">
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required">
<tb-cf-alarm-rule-condition formControlName="condition" [arguments]="arguments" [required]="required" [testScript]="testScript" [isClearCondition]="isClearCondition">
</tb-cf-alarm-rule-condition>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="tb-form-row space-between column-xs">

1
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss

@ -14,6 +14,7 @@
* limitations under the License.
*/
:host {
width: 100%;
.tb-alarm-rule-details, .tb-alarm-rule-dashboard {
padding: 4px;
&.title {

8
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts

@ -34,6 +34,7 @@ import {
AlarmRuleDetailsDialogData
} from "@home/components/alarm-rules/alarm-rule-details-dialog.component";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({
selector: 'tb-cf-alarm-rule',
@ -65,6 +66,13 @@ export class CfAlarmRuleComponent implements ControlValueAccessor, OnInit, Valid
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input()
@coerceBoolean()
isClearCondition = false;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
private modelValue: AlarmRule;
alarmRuleFormGroup = this.fb.group({

6
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html

@ -34,7 +34,7 @@
</mat-select>
</mat-form-field>
</div>
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" required class="flex-1">
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" [testScript]="testScript" required class="flex-1">
</tb-cf-alarm-rule>
</div>
<button *ngIf="!disabled"
@ -57,9 +57,7 @@
<button mat-stroked-button color="primary"
[disabled]="createAlarmRulesFormArray().controls.length > 4"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'alarm-rule.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
(click)="addCreateAlarmRule()">
{{ 'alarm-rule.add-create-alarm-rule' | translate }}
</button>
</div>

5
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts

@ -33,6 +33,7 @@ import { CalculatedFieldArgument } from "@shared/models/calculated-field.models"
import { AlarmSeverityNotificationColors } from "@shared/models/notification.models";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { coerceBoolean } from "@shared/decorators/coercion";
import { Observable } from "rxjs";
@Component({
selector: 'tb-create-cf-alarm-rules',
@ -60,6 +61,8 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
@Input()
arguments: Record<string, CalculatedFieldArgument>;
@Input({required: true})
testScript: (expression: string) => Observable<string>;
alarmSeverities = Object.keys(AlarmSeverity);
alarmSeverityEnum = AlarmSeverity;
@ -157,7 +160,7 @@ export class CreateCfAlarmRulesComponent implements ControlValueAccessor, Valida
}
public validate(c: UntypedFormControl) {
return (this.createAlarmRulesFormArray().length && this.createAlarmRulesFormGroup.valid) ? null : {
return this.createAlarmRulesFormArray().length ? null : {
createAlarmRules: {
valid: false,
},

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

@ -67,7 +67,10 @@
<section class="tb-form-panel">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.filter' | translate }}</div>
<div class="tb-form-panel-title"
tb-hint-tooltip-icon="{{ filterFormGroup.get('valueType').value === entityKeyValueTypeEnum.DATE_TIME ? ('alarm-rule.date-time-hint' | translate) : '' }}">
{{ 'alarm-rule.filter' | translate }}
</div>
<tb-toggle-select formControlName="operation"
selectMediaBreakpoint="xs">
<tb-toggle-option [value]="ComplexOperation.AND">{{ complexOperationTranslationMap.get(ComplexOperation.AND) | translate }}</tb-toggle-option>

4
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html

@ -71,9 +71,7 @@
</section>
<button mat-button mat-raised-button color="primary"
(click)="addFilter()"
type="button"
matTooltip="{{ 'alarm-rule.add-filter' | translate }}"
matTooltipPosition="above">
type="button">
{{ 'alarm-rule.add-filter' | translate }}
</button>

8
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html

@ -69,17 +69,13 @@
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(false)"
type="button"
matTooltip="{{ 'filter.add-filter' | translate }}"
matTooltipPosition="above">
type="button">
{{ 'action.add' | translate }}
</button>
<button mat-button mat-raised-button color="primary"
[class.!hidden]="disabled"
(click)="addPredicate(true)"
type="button"
matTooltip="{{ 'filter.add-complex-filter' | translate }}"
matTooltipPosition="above">
type="button">
{{ 'filter.add-complex' | translate }}
</button>
</div>

21
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts

@ -24,7 +24,7 @@ import { TranslateService } from '@ngx-translate/core';
import { Direction } from '@shared/models/page/sort-order';
import { MatDialog } from '@angular/material/dialog';
import { PageLink } from '@shared/models/page/page-link';
import { Observable, of } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
import { Store } from '@ngrx/store';
@ -60,6 +60,7 @@ import { isObject } from '@core/utils';
import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service';
import { DatePipe } from '@angular/common';
import { UtilsService } from "@core/services/utils.service";
import { ActionNotificationShow } from "@core/notification/notification.actions";
export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedField> {
@ -256,6 +257,19 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
this.importExportService.openCalculatedFieldImportDialog()
.pipe(
filter(Boolean),
switchMap(calculatedField => {
if (calculatedField.type === CalculatedFieldType.ALARM) {
this.store.dispatch(new ActionNotificationShow({
message: this.translate.instant('calculated-fields.hint.import-invalid-calculated-field-type'),
type: 'error',
verticalPosition: 'top',
horizontalPosition: 'left',
duration: 5000
}));
return EMPTY;
}
return of(calculatedField);
}),
switchMap(calculatedField => this.getCalculatedFieldDialog(this.updateImportedCalculatedField(calculatedField), 'action.add', true)),
filter(Boolean),
switchMap(calculatedField => this.calculatedFieldsService.saveCalculatedField(calculatedField)),
@ -295,9 +309,10 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
).subscribe(() => this.updateData());
}
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true): Observable<string> {
private getTestScriptDialog(calculatedField: CalculatedField, argumentsObj?: CalculatedFieldEventArguments, openCalculatedFieldEdit = true, expression?: string): Observable<string> {
if (
calculatedField.type === CalculatedFieldType.SCRIPT ||
calculatedField.type === CalculatedFieldType.RELATED_ENTITIES_AGGREGATION ||
(calculatedField.type === CalculatedFieldType.PROPAGATION && calculatedField.configuration.applyExpressionToResolvedArguments === true)
) {
const resultArguments = Object.keys(calculatedField.configuration.arguments).reduce((acc, key) => {
@ -313,7 +328,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig<CalculatedFie
panelClass: ['tb-dialog', 'tb-fullscreen-dialog', 'tb-fullscreen-dialog-gt-xs'],
data: {
arguments: resultArguments,
expression: (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression,
expression: expression ?? (calculatedField.configuration as CalculatedFieldScriptConfiguration | PropagationWithExpression).expression,
argumentsEditorCompleter: getCalculatedFieldArgumentsEditorCompleter(calculatedField.configuration.arguments),
argumentsHighlightRules: getCalculatedFieldArgumentsHighlights(calculatedField.configuration.arguments),
openCalculatedFieldEdit

25
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html

@ -24,15 +24,8 @@
</div>
}
<div class="tb-form-panel no-border no-padding">
@if (!isOutputKey) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: {
label: 'calculated-fields.argument-name',
required: 'calculated-fields.hint.argument-name-required',
duplicate: 'calculated-fields.hint.argument-name-duplicate',
pattern: 'calculated-fields.hint.argument-name-pattern',
maxlength: 'calculated-fields.hint.argument-name-max-length',
forbidden: 'calculated-fields.hint.argument-name-forbidden'
}"></ng-container>
@if (!watchKeyChange) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: argumentNameContext"></ng-container>
}
<ng-container>
@if (!hiddenEntityTypes) {
@ -105,7 +98,7 @@
<ng-container [ngTemplateOutlet]="timeseriesKeyAutocomplete"/>
}
<ng-template #timeseriesKeyAutocomplete>
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter"/>
<tb-entity-key-autocomplete class="flex-1" formControlName="key" [dataKeyType]="DataKeyType.timeseries" [entityFilter]="entityFilter" [enableAutocomplete]="!enableAutocomplete"/>
</ng-template>
</div>
} @else {
@ -132,6 +125,7 @@
<tb-entity-key-autocomplete
formControlName="key"
class="flex-1"
[enableAutocomplete]="!enableAutocomplete"
[dataKeyType]="DataKeyType.attribute"
[entityFilter]="entityFilter"
[keyScopeType]="argumentFormGroup.get('refEntityKey').get('scope').value"
@ -140,15 +134,8 @@
}
}
</ng-container>
@if (isOutputKey) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: {
label: 'calculated-fields.output-key',
required: 'calculated-fields.hint.output-key-required',
duplicate: 'calculated-fields.hint.output-key-duplicate',
pattern: 'calculated-fields.hint.output-key-pattern',
maxlength: 'calculated-fields.hint.output-key-max-length',
forbidden: 'calculated-fields.hint.output-key-forbidden'
}"></ng-container>
@if (watchKeyChange) {
<ng-container *ngTemplateOutlet="argumentNameTemplate; context: argumentNameContext"></ng-container>
}
@if (refEntityKeyFormGroup.get('type').value !== ArgumentType.Rolling) {
@if (!hiddenDefaultValue) {

34
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts

@ -71,7 +71,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() ownerId: EntityId;
@Input() isScript: boolean;
@Input() usedArgumentNames: string[];
@Input() isOutputKey = false;
@Input() watchKeyChange = false;
@Input() hiddenEntityTypes = false;
@Input() hiddenEntityKeyTypes = false;
@Input() hiddenDefaultValue = false;
@ -80,6 +80,14 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() predefinedEntityFilter: EntityFilter;
@Input() forbiddenNames = FORBIDDEN_NAMES;
@Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[];
@Input() argumentNameContext: {[key: string]: string} = {
label: 'calculated-fields.argument-name',
required: 'calculated-fields.hint.argument-name-required',
duplicate: 'calculated-fields.hint.argument-name-duplicate',
pattern: 'calculated-fields.hint.argument-name-pattern',
maxlength: 'calculated-fields.hint.argument-name-max-length',
forbidden: 'calculated-fields.hint.argument-name-forbidden'
};
@ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
@ -107,6 +115,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
entityFilter: EntityFilter;
entityNameSubject = new BehaviorSubject<string>(null);
enableAutocomplete = false;
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
readonly ArgumentType = ArgumentType;
readonly DataKeyType = DataKeyType;
@ -155,7 +165,9 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
this.toggleByEntityKeyType(this.argument.refEntityKey?.type);
this.setInitialEntityKeyType();
this.setInitialEntityType();
this.setWatchKeyChange();
if (this.watchKeyChange) {
this.setWatchKeyChange();
}
if (this.defaultValueRequired) {
this.argumentFormGroup.get('defaultValue').addValidators(Validators.required);
@ -202,6 +214,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);
argumentType = ArgumentEntityType.Owner;
} else if (this.argument.refEntityId?.entityType) {
argumentType = this.argument.refEntityId.entityType;
@ -270,6 +283,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.updatedRefEntityIdState(type);
if (!this.enableAttributeScopeSelection) {
this.refEntityKeyFormGroup.get('scope').setValue(AttributeScope.SERVER_SCOPE);
@ -299,15 +313,13 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
}
private setWatchKeyChange(): void {
if (this.isOutputKey) {
this.refEntityKeyFormGroup.get('key').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((key) => {
if (this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').setValue(key);
}
});
}
this.refEntityKeyFormGroup.get('key').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((key) => {
if (this.argumentFormGroup.get('argumentName').pristine) {
this.argumentFormGroup.get('argumentName').setValue(key);
}
});
}
private updatedRefEntityIdState(type: ArgumentEntityType, emitEvent = true): void {

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html

@ -120,10 +120,10 @@
<mat-row *matRowDef="let argument; columns: displayColumns"></mat-row>
</table>
<div [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="tb-prompt flex flex-1 items-end justify-center">
class="tb-prompt flex flex-1 items-end justify-center min-h-14">
{{ 'calculated-fields.no-arguments' | translate }}
</div>
@if (errorText) {
@if (errorText || (dataSource.isEmpty() | async)) {
<tb-error noMargin [error]="errorText | translate" class="flex h-9 items-center pl-3"/>
}
</div>
@ -134,7 +134,7 @@
color="primary"
#button
(click)="manageArgument($event, button)"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF"
[disabled]="maxArgumentsPerCF > 0 && argumentsFormArray.length >= maxArgumentsPerCF || disabledAddButton"
>
{{ 'calculated-fields.add-argument' | translate }}
</button>

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts

@ -87,6 +87,8 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
@Input() entityName: string;
@Input() ownerId: EntityId;
@Input() isScript: boolean;
@Input() disabledAddButton = false;
@Input() watchKeyChange = false;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -181,6 +183,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
tenantId: this.tenantId,
entityName: this.entityName,
ownerId: this.ownerId,
watchKeyChange: this.watchKeyChange,
usedArgumentNames: this.argumentsFormArray.value.map(({ argumentName }) => argumentName).filter(name => name !== argument.argumentName),
};
this.popoverComponent = this.popoverService.displayPopover({

10
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts

@ -93,7 +93,15 @@ export class PropagateArgumentsTableComponent extends CalculatedFieldArgumentsTa
this.displayColumns = ['name', 'type', 'key', 'actions'];
this.panelAdditionalCtx = {
argumentEntityTypes: [ArgumentEntityType.Current],
isOutputKey: true,
argumentNameContext: {
label: 'calculated-fields.output-key',
required: 'calculated-fields.hint.output-key-required',
duplicate: 'calculated-fields.hint.output-key-duplicate',
pattern: 'calculated-fields.hint.output-key-pattern',
maxlength: 'calculated-fields.hint.output-key-max-length',
forbidden: 'calculated-fields.hint.output-key-forbidden'
},
watchKeyChange: true,
forbiddenNames: [...FORBIDDEN_NAMES, 'propagationCtx'],
};
}

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -88,6 +88,7 @@
[entityId]="data.entityId"
[entityName]="data.entityName"
[tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {

6
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -105,19 +105,19 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}
}
onTestScript(): Observable<string> {
onTestScript(expression?: string): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false);
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false, expression);
}),
takeUntilDestroyed(this.destroyRef)
)
}
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
return this.data.getTestScriptDialogFn(this.fromGroupValue, null, false, expression);
}
private applyDialogData(): void {

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html

@ -34,6 +34,7 @@
</div>
<tb-calculated-field-metrics-table formControlName="metrics"
simpleMode
[testScript]="testScript"
[arguments]="arguments$ | async"
></tb-calculated-field-metrics-table>
</div>

4
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts

@ -42,7 +42,7 @@ import { deepClone, isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { merge } from 'rxjs';
import { merge, Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import _moment from 'moment';
@ -85,6 +85,8 @@ export class EntityAggregationComponentComponent implements ControlValueAccessor
@Input({required: true})
entityName: string;
@Input() testScript: (expression?: string) => Observable<string>;
readonly minAllowedAggregationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedAggregationIntervalInSecForCF;
readonly DayInSec = DAY / SECOND;

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html

@ -176,7 +176,7 @@
</div>
</ng-container>
<ng-container>
@if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current) {
@if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) {
<div class="tb-form-row">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{'calculated-fields.hint.perimeter-attribute-key' | translate}}">
{{ 'calculated-fields.perimeter-attribute-key' | translate }}
@ -204,6 +204,7 @@
</mat-form-field>
} @else {
<tb-entity-key-autocomplete class="flex-1" formControlName="perimeterKeyName"
[enableAutocomplete]="!enableAutocomplete"
[dataKeyType]="DataKeyType.attribute" [entityFilter]="entityFilter"
[keyScopeType]="AttributeScope.SERVER_SCOPE"/>
}

13
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts

@ -97,6 +97,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
entityFilter: EntityFilter;
entityNameSubject = new BehaviorSubject<string>(null);
enableAutocomplete = false;
readonly ArgumentEntityType = ArgumentEntityType;
readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[];
readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations;
@ -151,6 +153,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
} else {
this.addKey();
}
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) &&
this.refEntityIdFormGroup.get('entityType').value === ArgumentEntityType.Owner;
this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones);
this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type);
@ -265,18 +269,17 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit
)
.pipe(debounceTime(50), takeUntilDestroyed())
.subscribe(() => this.updateEntityFilter(this.entityType));
this.refEntityIdFormGroup.get('id').valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()).subscribe(() => this.geofencingFormGroup.get('perimeterKeyName').reset(''));
}
private observeEntityTypeChanges(): void {
this.refEntityIdFormGroup.get('entityType').valueChanges
.pipe(distinctUntilChanged(), takeUntilDestroyed())
.subscribe(type => {
this.enableAutocomplete = (this.entityId.entityType === EntityType.DEVICE_PROFILE || this.entityId.entityType === EntityType.ASSET_PROFILE) && type === ArgumentEntityType.Owner;
this.geofencingFormGroup.get('refEntityId').get('id').setValue(null);
const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery;
this.geofencingFormGroup.get('refEntityId')
.get('id')[isEntityWithId ? 'enable' : 'disable']();
this.geofencingFormGroup.get('perimeterKeyName').reset('');
const isEntityWithId = !!type && ![ArgumentEntityType.Tenant, ArgumentEntityType.Current, ArgumentEntityType.Owner].includes(type);
this.geofencingFormGroup.get('refEntityId').get('id')[isEntityWithId ? 'enable' : 'disable']();
if (!isEntityWithId) {
this.entityNameSubject.next(null);
}

46
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html

@ -27,7 +27,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-required' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-required' | translate"
class="tb-error">
warning
</mat-icon>
@ -35,7 +35,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-duplicate' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-duplicate' | translate"
class="tb-error">
warning
</mat-icon>
@ -43,7 +43,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-pattern' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-pattern' | translate"
class="tb-error">
warning
</mat-icon>
@ -51,7 +51,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-max-length' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-max-length' | translate"
class="tb-error">
warning
</mat-icon>
@ -59,7 +59,7 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'calculated-fields.hint.name-forbidden' | translate"
[matTooltip]="'calculated-fields.metrics.metric-name-forbidden' | translate"
class="tb-error">
warning
</mat-icon>
@ -105,7 +105,24 @@
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!arguments.length"
(click)="onTestScript('filter')">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript('filter')"
[disabled]="!arguments.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
</ng-template>
</mat-expansion-panel>
</div>
@ -146,7 +163,7 @@
} @else {
<tb-js-func required
formControlName="function"
functionName="filter"
functionName="map"
[functionArgs]="functionArgs"
[disableUndefinedCheck]="true"
[scriptLanguage]="ScriptLanguage.TBEL"
@ -157,7 +174,24 @@
<div toolbarPrefixButton
class="tb-primary-background tbel-script-lang-chip">{{ 'api-usage.tbel' | translate }}
</div>
<button toolbarSuffixButton
mat-icon-button
matTooltip="{{ 'calculated-fields.test-script-function' | translate }}"
matTooltipPosition="above"
class="tb-mat-32"
[disabled]="!arguments.length"
(click)="onTestScript('input.function')">
<mat-icon class="material-icons" color="primary">bug_report</mat-icon>
</button>
</tb-js-func>
<div>
<button mat-button mat-raised-button color="primary"
type="button"
(click)="onTestScript('input.function')"
[disabled]="!arguments.length">
{{ 'calculated-fields.test-script-function' | translate }}
</button>
</div>
}
</ng-container>
@if (simpleMode) {

11
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts

@ -33,6 +33,7 @@ import { EntityFilter } from '@shared/models/query/query.models';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import { Observable } from "rxjs";
interface CalculatedFieldAggMetricValuePanel extends CalculatedFieldAggMetricValue {
allowFilter: boolean;
@ -52,6 +53,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
@Input() simpleMode: boolean;
@Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules;
@Input({required: true}) testScript: (expression?: string) => Observable<string>;
metricDataApplied = output<CalculatedFieldAggMetricValue>();
filterExpanded = false;
@ -81,7 +83,7 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
constructor(
private fb: FormBuilder,
private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent>
private popover: TbPopoverComponent<CalculatedFieldMetricsPanelComponent>,
) {
this.observeFilterAllowChange();
this.observeInputTypeChange();
@ -163,4 +165,11 @@ export class CalculatedFieldMetricsPanelComponent implements OnInit {
this.metricForm.get('input.key').markAsTouched();
}
}
onTestScript(scriptFunc: 'filter' | 'input.function') {
this.testScript(this.metricForm.get(scriptFunc).value).subscribe(expression => {
this.metricForm.get(scriptFunc).setValue(expression);
this.metricForm.get(scriptFunc).markAsDirty();
});
}
}

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts

@ -56,6 +56,7 @@ import {
} from '@home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import { Observable } from "rxjs";
@Component({
selector: 'tb-calculated-field-metrics-table',
@ -80,6 +81,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
@Input() editorCompleter: TbEditorCompleter;
@Input() highlightRules: AceHighlightRules;
@Input({transform: booleanAttribute}) simpleMode: boolean = false;
@Input({required: true}) testScript: (expression?: string) => Observable<string>;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ -166,6 +168,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu
editorCompleter: this.editorCompleter,
highlightRules: this.highlightRules,
simpleMode: this.simpleMode,
testScript: this.testScript
};
this.popoverComponent = this.popoverService.displayPopover({
trigger,

1
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html

@ -55,6 +55,7 @@
{{ 'calculated-fields.metrics.metrics' | translate }}
</div>
<tb-calculated-field-metrics-table formControlName="metrics"
[testScript]="testScript"
[arguments]="arguments$ | async"
[highlightRules]="argumentsHighlightRules$ | async"
[editorCompleter]="argumentsEditorCompleter$ | async"

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts

@ -73,6 +73,9 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
@Input({required: true})
entityName: string;
@Input({required: true})
testScript: (expression?: string) => Observable<string>;
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType;

4
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html

@ -33,11 +33,11 @@
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="entity.tenantId"/>
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="ownerId"/>
</mat-tab>
<mat-tab #alarmRules="matTab"
label="{{'alarm-rule.alarm-rules' | translate }}">
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table>
<tb-alarm-rules-table [active]="alarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="ownerId"></tb-alarm-rules-table>
</mat-tab>
}
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">

8
ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts

@ -19,6 +19,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { AssetInfo } from '@app/shared/models/asset.models';
import { EntityId } from "@shared/models/id/entity-id";
@Component({
selector: 'tb-asset-tabs',
@ -27,6 +28,8 @@ import { AssetInfo } from '@app/shared/models/asset.models';
})
export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
ownerId: EntityId;
constructor(protected store: Store<AppState>) {
super(store);
}
@ -35,4 +38,9 @@ export class AssetTabsComponent extends EntityTabsComponent<AssetInfo> {
super.ngOnInit();
}
protected setEntity(entity: AssetInfo) {
this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId;
super.setEntity(entity);
}
}

4
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html

@ -33,10 +33,10 @@
</mat-tab>
@if (authUser.authority === authorities.TENANT_ADMIN) {
<mat-tab label="{{ 'entity.type-calculated-fields' | translate }}" #calculatedFieldsTab="matTab">
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="entity.tenantId"/>
<tb-calculated-fields-table [active]="calculatedFieldsTab.isActive" [entityId]="entity.id" [ownerId]="ownerId"/>
</mat-tab>
<mat-tab #calculatedFieldsAlarmRules="matTab" label="{{'alarm-rule.alarm-rules-tab' | translate }}">
<tb-alarm-rules-table [active]="calculatedFieldsAlarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="entity.tenantId"></tb-alarm-rules-table>
<tb-alarm-rules-table [active]="calculatedFieldsAlarmRules.isActive" [entityId]="entity.id" [entityName]="entity.name" [ownerId]="ownerId"></tb-alarm-rules-table>
</mat-tab>
}
<mat-tab label="{{ 'alarm.alarms' | translate }}" #alarmsTab="matTab">

8
ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts

@ -19,6 +19,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DeviceInfo } from '@shared/models/device.models';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { EntityId } from "@shared/models/id/entity-id";
@Component({
selector: 'tb-device-tabs',
@ -27,6 +28,8 @@ import { EntityTabsComponent } from '../../components/entity/entity-tabs.compone
})
export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
ownerId: EntityId;
constructor(protected store: Store<AppState>) {
super(store);
}
@ -35,4 +38,9 @@ export class DeviceTabsComponent extends EntityTabsComponent<DeviceInfo> {
super.ngOnInit();
}
protected setEntity(entity: DeviceInfo) {
this.ownerId = entity.customerId.id !== this.nullUid ? entity.customerId : entity.tenantId;
super.setEntity(entity);
}
}

2
ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html

@ -43,7 +43,7 @@
@for (key of filteredKeys$ | async; track key) {
<mat-option [value]="key"><span [innerHTML]="key | highlight: searchText"></span></mat-option>
} @empty {
@if (!this.keyControl.value) {
@if (!this.keyControl.value && enableAutocomplete) {
<mat-option [value]="''">{{ 'entity.no-keys-found' | translate }}</mat-option>
}
}

7
ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts

@ -66,6 +66,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
@Input() placeholder = this.translate.instant('action.set');
@Input() requiredText = this.translate.instant('common.hint.key-required');
@Input() enableAutocomplete = true;
entityFilter = input.required<EntityFilter>();
dataKeyType = input.required<DataKeyType>();
@ -81,12 +82,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
keys$ = this.keyInputSubject.asObservable()
.pipe(
switchMap(() => {
if (!this.enableAutocomplete) {
return of([] as string[]);
}
return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({
pageLink: { page: 0, pageSize: 100 },
entityFilter: this.entityFilter(),
}, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true});
}),
map(result => {
if (Array.isArray(result)) {
return result;
}
this.cachedResult = result;
switch (this.dataKeyType()) {
case DataKeyType.attribute:

2
ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts

@ -204,7 +204,7 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit,
case EntityType.CALCULATED_FIELD:
this.placeholder = this.required ? this.translate.instant('alarm.enter-alarm-rule-type')
: this.translate.instant('alarm-rule.any-type');
this.secondaryPlaceholder = '+' + this.translate.instant('alarm-rule.alarm-rule');
this.secondaryPlaceholder = '+' + this.translate.instant('alarm-rule.alarm-type');
this.noSubtypesMathingText = 'alarm-rule.no-alarm-rule-types-matching';
this.subtypeListEmptyText = 'alarm-rule.alarm-rule-type-list-empty';
break;

8
ui-ngx/src/app/shared/models/alarm-rule.models.ts

@ -27,6 +27,8 @@ import {
StringOperation
} from "@shared/models/query/query.models";
import { EntityType } from "@shared/models/entity-type.models";
import { Observable } from "rxjs";
import { CalculatedField } from "@shared/models/calculated-field.models";
export enum AlarmRuleScheduleType {
ANY_TIME = 'ANY_TIME',
@ -157,3 +159,9 @@ export interface AlarmRuleFilterConfig {
entityType?: EntityType;
entities?: Array<string>;
}
export const alarmRuleDefaultScript =
'// Sample expression for an alarm rule: triggers when temperature is above 20 degree\n' +
'return temperature > 20;'
export type AlarmRuleTestScriptFn = (calculatedField: CalculatedField, expression: string, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;

9
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -65,11 +65,18 @@ export interface CalculatedFieldAlarmRule extends BaseCalculatedField {
configuration: CalculatedFieldAlarmRuleConfiguration;
}
export interface CalculatedFieldRelatedEntityAggregation extends BaseCalculatedField {
type: CalculatedFieldType.RELATED_ENTITIES_AGGREGATION;
configuration: CalculatedFieldRelatedAggregationConfiguration;
}
export type CalculatedField =
| CalculatedFieldSimple
| CalculatedFieldScript
| CalculatedFieldGeofencing
| CalculatedFieldPropagation
| CalculatedFieldRelatedEntityAggregation
| CalculatedFieldAlarmRule;
export enum CalculatedFieldType {
@ -489,7 +496,7 @@ export interface CalculatedFieldArgumentValue extends CalculatedFieldArgument {
entityName?: string;
}
export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean) => Observable<string>;
export type CalculatedFieldTestScriptFn = (calculatedField: CalculatedField, argumentsObj?: Record<string, unknown>, closeAllOnSave?: boolean, expression?: string) => Observable<string>;
export interface CalculatedFieldTestScriptInputParams {
arguments: CalculatedFieldEventArguments;

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

@ -805,7 +805,7 @@
"idCopiedMessage": "Asset Id has been copied to clipboard",
"select-asset": "Select asset",
"no-assets-matching": "No assets matching '{{entity}}' were found.",
"asset-required": "Asset is required",
"asset-required": "Asset is required.",
"name-starts-with": "Asset name expression",
"help-text": "Use '%' according to need: '%asset_name_contains%', '%asset_name_ends', 'asset_starts_with'.",
"import": "Import assets",
@ -1214,12 +1214,17 @@
"copy-output-key": "Copy output key",
"aggregation-path-related-entities": "Aggregation path to related entities",
"deduplication-interval": "Deduplication interval",
"deduplication-interval-min": "Deduplication interval should be at least {{ sec }} second.",
"deduplication-interval-min": "Deduplication interval should be at least {{ sec }} seconds.",
"deduplication-interval-required": "Deduplication interval is required.",
"metrics": {
"metrics": "Metrics",
"metrics-empty": "At least one metric must be configured.",
"metric-name": "Metric name",
"metric-name-required": "Metric name is required.",
"metric-name-pattern": "Metric name is invalid.",
"metric-name-duplicate": "Metric name with such name already exists.",
"metric-name-max-length": "Metric name should be less than 256 characters.",
"metric-name-forbidden": "Metric name is reserved and cannot be used.",
"copy-metric-name": "Copy metric name",
"aggregation": "Aggregation",
"aggregation-type": {
@ -1309,7 +1314,7 @@
"arguments-propagate-arguments-with-rolling": "'Time series rolling' type is incompatible with 'Arguments only' propagation.",
"arguments-propagate-argument-entity-type": "Entity type is incompatible with 'Arguments only' propagation.",
"arguments-propagate-argument-must-current-entity": "At least one argument must be configured with the 'Current entity' source entity type.",
"arguments-empty": "Arguments should not be empty.",
"arguments-empty": "At least one argument should be specified.",
"expression-required": "Expression is required.",
"expression-invalid": "Expression is invalid",
"expression-max-length": "Expression length should be less than 255 characters.",
@ -1349,13 +1354,14 @@
"max-geofencing-zone": "Maximum number of geofencing zones reached.",
"zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.",
"zone-group-refresh-interval-required": "Zone groups refresh interval is required.",
"zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second.",
"zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} seconds.",
"propagation-path-related-entities": "Defines a direct, single-level path to a related entity based on the selected direction and relation type.",
"data-propagate": "Defines the data to be propagated from the arguments configured below. 'Arguments only' uses the retrieved data directly, while 'Expression result' calculates a new value from that data.",
"aggregation-path-related-entities": "Defines a single-level aggregation path via direct relations with parent or child entities based on direction and relation type. Only relations between device, asset, customer, and tenant entities are supported.",
"arguments-aggregation": "Defines input parameters used for filtering and aggregation.",
"setting-arguments-aggregation": "Data will be fetched from related entities configured in aggregation path.",
"metrics": "Defines metrics aggregated based on the configured arguments."
"metrics": "Defines metrics aggregated based on the configured arguments.",
"import-invalid-calculated-field-type": "Unable to import calculated field: Invalid calculated field type."
}
},
"alarm-rule": {
@ -1409,6 +1415,7 @@
"value-type": "Value type",
"general": "General",
"filter": "Filter",
"date-time-hint": "The argument must be in epoch milliseconds. Example: 1698839340000 equals 2023-11-01 12:49:00 UTC.",
"operation": "Operation",
"value-source": "Value source",
"value": "Value",
@ -1445,6 +1452,7 @@
"schedule-time-from": "From",
"schedule-time-to": "To",
"schedule-days-of-week-required": "At least one day of week should be selected.",
"tbel": "TBEL",
"expression-type": {
"simple": "Simple",
"script": "Script"
@ -1467,13 +1475,14 @@
"alarm-rule-mobile-dashboard-hint": "Used by mobile application as an alarm details dashboard",
"alarm-rule-no-mobile-dashboard": "No dashboard selected",
"alarm-rule-condition": "Alarm rule condition",
"enter-alarm-rule-condition-prompt": "Please add alarm rule condition",
"enter-alarm-rule-condition-prompt": "Add alarm rule creating condition",
"enter-alarm-rule-clear-condition-prompt": "Add alarm rule clearing condition",
"edit-alarm-rule-condition": "Edit alarm rule condition",
"condition-type": "Condition type",
"select-alarm-severity": "Select alarm severity",
"add-create-alarm-rule-prompt": "Please add create alarm rule",
"add-create-alarm-rule": "Add create condition",
"add-clear-alarm-rule": "Add clear condition",
"add-create-alarm-rule-prompt": "At least one creation condition should be specified",
"add-create-alarm-rule": "Add creation condition",
"add-clear-alarm-rule": "Add clearing condition",
"condition-duration": "Condition duration",
"condition-duration-value": "Duration value",
"condition-duration-time-unit": "Time unit",
@ -1485,9 +1494,9 @@
"condition-repeating-value-range": "Count of events should be in a range from 1 to 2147483647.",
"condition-repeating-value-pattern": "Count of events should be integers.",
"condition-repeating-value-required": "Count of events is required.",
"create-conditions": "Create conditions",
"clear-condition": "Clear condition",
"no-clear-alarm-rule": "No clear condition configured",
"create-conditions": "Creation conditions",
"clear-condition": "Clearing condition",
"no-clear-alarm-rule": "Clearing condition not configured",
"advanced-settings": "Advanced settings",
"propagate-alarm": "Propagate alarm to related entities",
"alarm-rule-relation-types-list": "Relation types",
@ -1501,7 +1510,8 @@
"no-alarm-rule-types-matching": "No alarm rule types matching '{{entitySubtype}}' were found.",
"alarm-rule-type-list-empty": "No alarm rule types selected.",
"alarm-rule-type-list": "Alarm rule type list",
"alarm-rule-entity-list": "Entity list"
"alarm-rule-entity-list": "Entity list",
"import-invalid-alarm-rule-type": "Unable to import alarm rule: Invalid alarm rule type."
},
"ai-models": {
"ai-models": "AI models",
@ -1714,7 +1724,7 @@
"idCopiedMessage": "Customer Id has been copied to clipboard",
"select-customer": "Select customer",
"no-customers-matching": "No customers matching '{{entity}}' were found.",
"customer-required": "Customer is required",
"customer-required": "Customer is required.",
"select-default-customer": "Select default customer",
"default-customer": "Default customer",
"default-customer-required": "Default customer is required in order to debug dashboard on Tenant level",

Loading…
Cancel
Save