Browse Source

Merge remote-tracking branch 'upstream/master' into support-user-sync

pull/14352/head
Nikita Mazurenko 10 months ago
parent
commit
bd141159fe
  1. 2
      application/src/main/data/json/system/widget_types/timeseries_table.json
  2. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  3. 25
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts
  4. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.ts
  5. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-table-header.component.ts
  6. 81
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts
  7. 19
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  8. 12
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.ts
  9. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.html
  10. 20
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition.component.ts
  11. 2
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.html
  12. 1
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.scss
  13. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule.component.ts
  14. 6
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  15. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.ts
  16. 5
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  17. 4
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-list.component.html
  18. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-list.component.html
  19. 21
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts
  20. 25
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.html
  21. 34
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  22. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html
  23. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts
  24. 10
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/propagate-arguments-table.component.ts
  25. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  26. 6
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  27. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  28. 4
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.ts
  29. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  30. 13
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.ts
  31. 46
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.html
  32. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-panel.component.ts
  33. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts
  34. 158
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html
  35. 27
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  36. 1
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  37. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  38. 10
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts
  39. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html
  40. 23
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  41. 35
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  42. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts
  43. 62
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  44. 4
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.html
  45. 8
      ui-ngx/src/app/modules/home/pages/asset/asset-tabs.component.ts
  46. 4
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.html
  47. 8
      ui-ngx/src/app/modules/home/pages/device/device-tabs.component.ts
  48. 4
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  49. 46
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html
  50. 2
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html
  51. 7
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts
  52. 2
      ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts
  53. 8
      ui-ngx/src/app/shared/models/alarm-rule.models.ts
  54. 9
      ui-ngx/src/app/shared/models/calculated-field.models.ts
  55. 5
      ui-ngx/src/assets/dashboard/api_usage.json
  56. 58
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  57. 6
      ui-ngx/src/form.scss

2
application/src/main/data/json/system/widget_types/timeseries_table.json

@ -17,7 +17,7 @@
"latestDataKeySettingsDirective": "tb-timeseries-table-latest-key-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-timeseries-table-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":[]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"tabSortKey\":\"timestamp\"},\"title\":\"Time series table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"titleIcon\":null}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}],\"latestDataKeys\":[]}],\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"showCellActionsMenu\":true,\"reserveSpaceForHiddenAction\":\"true\",\"showTimestamp\":true,\"dateFormat\":{\"format\":\"yyyy-MM-dd HH:mm:ss\"},\"displayPagination\":true,\"useEntityLabel\":false,\"defaultPageSize\":10,\"pageStepCount\":3,\"pageStepIncrement\":10,\"hideEmptyLines\":false,\"disableStickyHeader\":false,\"useRowStyleFunction\":false,\"rowStyleFunction\":\"\",\"sortOrder\":{\"property\":\"createdTime\",\"direction\":\"DESC\"}},\"title\":\"Time series table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"configMode\":\"basic\",\"titleFont\":null,\"titleColor\":null,\"titleIcon\":null}"
},
"resources": [
{

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,

158
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html

@ -46,7 +46,7 @@
@if (hiddenName) {
<div class="grid items-start gap-3 has-[.simpleMode]:grid-cols-2 xs:grid-cols-1">
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
<ng-content select=".simpleMode"></ng-content>
<ng-container *ngTemplateOutlet="simpleContentTemplate"></ng-container>
</div>
} @else {
<div class="flex items-start gap-3 xs:flex-col xs:items-stretch">
@ -74,93 +74,94 @@
</mat-form-field>
<ng-container *ngTemplateOutlet="decimalsByDefaultField"></ng-container>
</div>
<ng-content select=".simpleMode"></ng-content>
<ng-container *ngTemplateOutlet="simpleContentTemplate"></ng-container>
}
}
</div>
<div class="tb-form-panel stroked" formGroupName="strategy">
<div class="flex flex-row items-center justify-between gap-2">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
<mat-expansion-panel class="tb-settings" [disabled]="outputForm.get('strategy.type').value === OutputStrategyType.RULE_CHAIN">
<mat-expansion-panel-header class="flex flex-row flex-wrap">
<mat-panel-title>
<div class="flex flex-1 flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
<div class="tb-form-panel-title" style="color: var(--mat-expansion-header-text-color)" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.strategy' | translate }}">
{{ 'calculated-fields.output-strategy.strategy' | translate }}
</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs" disablePagination appearance="fill" (click)="$event.stopPropagation()">
@for (outputStrategyType of OutputStrategyTypes; track outputStrategyType) {
<tb-toggle-option [value]="outputStrategyType">{{ OutputStrategyTypeTranslations.get(outputStrategyType) | translate }}</tb-toggle-option>
}
</tb-toggle-select>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
@if (outputForm.get('type').value === OutputType.Timeseries) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveTimeSeries">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-time-series' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-time-series</div>
</div>
</mat-slide-toggle>
</div>
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveLatest">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-latest-values' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-latest-values</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="saveAttribute">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-database' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-database</div>
</div>
</mat-slide-toggle>
</div>
}
</tb-toggle-select>
</div>
@if (outputForm.get('strategy.type').value === OutputStrategyType.IMMEDIATE) {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-normal" tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.processing-options' | translate }}">
{{ 'calculated-fields.output-strategy.processing-options' | translate }}
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="sendWsUpdate">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.send-web-sockets' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.send-web-sockets</div>
</div>
</mat-slide-toggle>
</div>
<mat-chip-listbox multiple>
@if (outputForm.get('type').value === OutputType.Timeseries) {
<mat-chip-option
[selected]="outputForm.get('strategy.saveTimeSeries').value"
[disabled]="outputForm.get('strategy.saveTimeSeries').disabled"
(click)="toggleChip('saveTimeSeries')">
{{ 'calculated-fields.output-strategy.save-time-series' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.saveLatest').value"
[disabled]="outputForm.get('strategy.saveLatest').disabled"
(click)="toggleChip('saveLatest')">
{{ 'calculated-fields.output-strategy.save-latest-values' | translate }}
</mat-chip-option>
} @else {
<mat-chip-option
[selected]="outputForm.get('strategy.saveAttribute').value"
[disabled]="outputForm.get('strategy.saveAttribute').disabled"
(click)="toggleChip('saveAttribute')">
{{ 'calculated-fields.output-strategy.save-database' | translate }}
</mat-chip-option>
}
<mat-chip-option
[selected]="outputForm.get('strategy.sendWsUpdate').value"
[disabled]="outputForm.get('strategy.sendWsUpdate').disabled"
(click)="toggleChip('sendWsUpdate')">
{{ 'calculated-fields.output-strategy.send-web-sockets' | translate }}
</mat-chip-option>
<mat-chip-option
[selected]="outputForm.get('strategy.processCfs').value"
[disabled]="outputForm.get('strategy.processCfs').disabled"
(click)="toggleChip('processCfs')">
{{ 'calculated-fields.output-strategy.save-calculated-fields' | translate }}
</mat-chip-option>
</mat-chip-listbox>
</div>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
<mat-slide-toggle class="mat-slide" formControlName="processCfs">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.save-calculated-fields' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.save-calculated-fields</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<tb-time-unit-input
required
subscriptSizing="dynamic"
appearance="outline"
sameWidthInputs
labelText="{{ 'calculated-fields.output-strategy.ttl' | translate }}"
requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}"
minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}"
formControlName="ttl">
<mat-icon class="help-icon mr-2 cursor-pointer"
aria-hidden="false"
aria-label="help-icon"
color="primary"
matSuffix
matTooltip="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
help_outline
</mat-icon>
</tb-time-unit-input>
@if (outputForm.get('type').value === OutputType.Attribute) {
<div class="tb-form-row flex-1">
<mat-slide-toggle class="mat-slide" formControlName="updateAttributesOnlyOnValueChange">
<div tb-hint-tooltip-icon="{{ (outputForm.get('strategy.updateAttributesOnlyOnValueChange').value
? 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change-enabled'
: 'calculated-fields.output-strategy.hint.update-attributes-only-on-value-change') | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.update-attributes-only-on-value-change</div>
</div>
</mat-slide-toggle>
</div>
} @else {
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="useCustomTtl">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.output-strategy.hint.ttl' | translate }}">
<div translate tbTruncateWithTooltip>calculated-fields.output-strategy.ttl</div>
</div>
</mat-slide-toggle>
<tb-time-unit-input
required
inlineField
class="medium-width"
requiredText="{{ 'calculated-fields.output-strategy.ttl-required' | translate }}"
minErrorText="{{ 'calculated-fields.output-strategy.ttl-min' | translate }}"
formControlName="ttl">
</tb-time-unit-input>
</div>
}
}
}
</mat-expansion-panel>
</div>
</div>
<ng-template #decimalsByDefaultField>
@ -172,3 +173,6 @@
}
</mat-form-field>
</ng-template>
<ng-template #simpleContentTemplate>
<ng-content select=".simpleMode"></ng-content>
</ng-template>

27
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -38,6 +38,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { merge } from 'rxjs';
import { deepClone } from '@core/utils';
@Component({
selector: 'tb-calculate-field-output',
@ -102,6 +104,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
sendWsUpdate: [true],
processCfs: [true],
updateAttributesOnlyOnValueChange: [true],
useCustomTtl: [false],
ttl: [0]
})
});
@ -116,7 +119,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
this.updatedStrategy();
});
this.outputForm.get('strategy.type').valueChanges
merge(
this.outputForm.get('strategy.type').valueChanges,
this.outputForm.get('strategy.useCustomTtl').valueChanges
)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.updatedStrategy();
@ -151,6 +157,9 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void {
this.outputForm.patchValue(value, {emitEvent: false});
if (value.type === OutputType.Timeseries && value.strategy?.type === OutputStrategyType.IMMEDIATE && value.strategy?.ttl) {
this.outputForm.get('strategy.useCustomTtl').setValue(true, {emitEvent: false});
}
this.outputForm.get('type').updateValueAndValidity({onlySelf: true});
}
@ -171,13 +180,6 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
}
}
toggleChip(controlName: string) {
const control = this.outputForm.get('strategy').get(controlName);
if (control && control.enabled) {
control.setValue(!control.value);
}
}
private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) {
if (this.simpleMode && 'name' in value) {
value.name = value.name?.trim() ?? '';
@ -185,6 +187,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
if (this.disableType) {
value.type = this.outputForm.get('type').value;
}
if (value.type === OutputType.Timeseries && value.strategy.type === OutputStrategyType.IMMEDIATE) {
value = deepClone(value);
delete (value.strategy as any).useCustomTtl
}
this.propagateChange(value);
}
@ -227,7 +233,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
} else {
this.outputForm.get('strategy.saveTimeSeries').enable({emitEvent: false});
this.outputForm.get('strategy.saveLatest').enable({emitEvent: false});
this.outputForm.get('strategy.ttl').enable({emitEvent: false});
this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: false});
if (this.outputForm.get('strategy.useCustomTtl').value) {
this.outputForm.get('strategy.ttl').enable({emitEvent: false});
}
}
}
}

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;

10
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts

@ -91,11 +91,11 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
onDataUpdated: (subscription) => {
const data = formattedDataFormDatasourceData(subscription.data);
this.apiUsages.forEach(key => {
const progress = data[0][key.maxLimit.key] !== 0 ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0;
const progress = (this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0) ? Math.min(100, ((data[0][key.current.key] / data[0][key.maxLimit.key]) * 100)) : 0;
key.progress = isFinite(progress) ? progress : 0;
key.status.value = data[0][key.status.key] ? data[0][key.status.key].toLowerCase() : 'enabled';
key.maxLimit.value = isFinite(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 && data[0][key.maxLimit.key] !== '' ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.current.value = isFinite(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
key.maxLimit.value = this.isFiniteNumber(data[0][key.maxLimit.key]) && data[0][key.maxLimit.key] !== 0 ? this.toShortNumber(data[0][key.maxLimit.key]) : '∞';
key.current.value = this.isFiniteNumber(data[0][key.current.key]) ? this.toShortNumber(data[0][key.current.key]) : 0;
});
this.cd.detectChanges();
}
@ -114,6 +114,10 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy {
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
}
private isFiniteNumber(value: any): boolean {
return typeof value === 'number' && isFinite(value);
}
updateState($event: MouseEvent, stateName: string) {
$event?.preventDefault();
if (stateName?.length) {

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html

@ -20,6 +20,7 @@
<div class="tb-form-panel-title" translate>widget-config.datasource</div>
<tb-entity-alias-select
class="tb-entity-alias-field"
showLabel
tbRequired
[appearance]="'outline'"
[aliasController]="context.aliasController"

23
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -115,15 +115,22 @@
<mat-slide-toggle class="mat-slide" formControlName="useEntityLabel">
{{ 'widgets.table.use-entity-label-tab-name' | translate }}
</mat-slide-toggle>
<div class="tb-form-row justify-between">
<div class="tb-form-row justify-between column-xs" formGroupName="sortOrder">
<div class="fixed-title-width" translate>widgets.table.sort-by</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="tabSortKey" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="TabSortKey.TIMESTAMP">{{ 'widgets.table.sort-timestamp-option' | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_ASC">{{ 'widgets.table.sort-asc' | translate }}</mat-option>
<mat-option [value]="TabSortKey.NAME_DESC">{{ 'widgets.table.sort-desc' | translate }}</mat-option>
</mat-select>
</mat-form-field>
<div class="flex flex-1 flex-row justify-end gap-2 ">
<mat-form-field class="flex-lt-lg medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="property" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="entityFields.createdTime.value">{{ entityFields.createdTime.name | translate }}</mat-option>
<mat-option [value]="entityFields.name.value">{{ entityFields.name.name | translate }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-lt-lg medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction" placeholder="{{ 'widget-config.set' | translate }}">
<mat-option [value]="Direction.ASC">{{ 'common.sort-asc' | translate }}</mat-option>
<mat-option [value]="Direction.DESC">{{ 'common.sort-desc' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel tb-slide-toggle">

35
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts

@ -20,7 +20,8 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { buildPageStepSizeValues } from '@home/components/widget/lib/table-widget.models';
import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-table-widget.component'
import { Direction } from '@shared/models/page/sort-order';
import { entityFields } from '@shared/models/entity.models';
@Component({
selector: 'tb-timeseries-table-widget-settings',
@ -29,7 +30,8 @@ import { TabSortKey } from '@app/modules/home/components/widget/lib/timeseries-t
})
export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsComponent {
TabSortKey = TabSortKey;
entityFields = entityFields;
Direction = Direction;
timeseriesTableWidgetSettingsForm: UntypedFormGroup;
pageStepSizeValues = [];
@ -62,13 +64,19 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
disableStickyHeader: false,
useRowStyleFunction: false,
rowStyleFunction: '',
tabSortKey: TabSortKey.TIMESTAMP
sortOrder: {
property: this.entityFields.createdTime.keyName,
direction: Direction.DESC
}
};
}
protected prepareInputSettings(settings: WidgetSettings): WidgetSettings {
settings.pageStepIncrement = settings.pageStepIncrement ?? settings.defaultPageSize;
settings.tabSortKey = settings.tabSortKey ?? TabSortKey.TIMESTAMP;
settings.sortOrder = {
property: settings.sortOrder?.property || this.entityFields.createdTime.keyName,
direction: settings.sortOrder?.direction || Direction.DESC
};
this.pageStepSizeValues = buildPageStepSizeValues(settings.pageStepCount, settings.pageStepIncrement);
return settings;
}
@ -99,7 +107,16 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
disableStickyHeader: [settings.disableStickyHeader, []],
useRowStyleFunction: [settings.useRowStyleFunction, []],
rowStyleFunction: [settings.rowStyleFunction, [Validators.required]],
tabSortKey: [settings.tabSortKey, []],
sortOrder: this.fb.group({
property: [
settings.sortOrder.property,
Validators.required
],
direction: [
settings.sortOrder.direction,
Validators.required
]
})
});
}
@ -107,6 +124,14 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
return ['useRowStyleFunction', 'displayPagination', 'pageStepCount', 'pageStepIncrement'];
}
protected prepareOutputSettings(settings: WidgetSettings): WidgetSettings {
settings.sortOrder = {
property: settings.sortOrder?.property || this.entityFields.createdTime.keyName,
direction: settings.sortOrder?.direction || Direction.DESC
};
return settings;
}
protected updateValidators(emitEvent: boolean, trigger: string) {
if (trigger === 'pageStepCount' || trigger === 'pageStepIncrement') {
this.timeseriesTableWidgetSettingsForm.get('defaultPageSize').reset();

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts

@ -64,6 +64,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
callbacks: EntityAliasSelectCallbacks;
@Input()
@coerceBoolean()
showLabel: boolean;
@ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete;

62
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -106,19 +106,14 @@ import { ComponentPortal } from '@angular/cdk/portal';
import { FormBuilder } from '@angular/forms';
import { DEFAULT_OVERLAY_POSITIONS } from '@shared/models/overlay.models';
import { DateFormatSettings, ValueFormatProcessor } from '@shared/models/widget-settings.models';
export enum TabSortKey {
NAME_ASC = 'NAME_ASC',
NAME_DESC = 'NAME_DESC',
TIMESTAMP = 'timestamp'
}
import { entityFields } from '@shared/models/entity.models';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean;
showMilliseconds: boolean;
hideEmptyLines: boolean;
dateFormat: DateFormatSettings;
tabSortKey: TabSortKey;
sortOrder: SortOrder;
}
interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings {
@ -400,33 +395,39 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.updateDatasources();
}
private getTabLabel(source: Datasource, entityLabelCache:Map<string,string>):string {
if (entityLabelCache.has(source.entityId)) {
return entityLabelCache.get(source.entityId);
}
const value = this.useEntityLabel
private getTabLabel(source: Datasource):string {
const value = this.useEntityLabel
? (source.entityLabel || source.entityName)
: source.entityName;
const translated = this.utils.customTranslation(value);
entityLabelCache.set(source.entityId, translated);
return translated;
return this.utils.customTranslation(value);
}
private sortDatasources(source: Datasource[], entityLabelCache:Map<string,string>): Datasource[] {
source.forEach(ds => this.getTabLabel(ds, entityLabelCache));
private sortDatasources(source: TimeseriesTableSource[]) {
const property = this.settings?.sortOrder?.property;
const direction = this.settings?.sortOrder?.direction;
const isAsc = direction === Direction.ASC;
if (property === entityFields.name.keyName) {
const collator = new Intl.Collator(undefined, {
sensitivity: "variant",
numeric: true,
ignorePunctuation: false
});
source.sort((a, b) => {
const valueA = a.displayName || '';
const valueB = b.displayName || '';
if (this.settings.tabSortKey === TabSortKey.TIMESTAMP) {
return source;
return isAsc
? collator.compare(valueA, valueB)
: collator.compare(valueB, valueA);
});
} else if (property === entityFields.createdTime.keyName) {
if (isAsc) {
source.reverse();
}
}
return source.sort((a, b) => {
const valueA = entityLabelCache.get(a.entityId);
const valueB = entityLabelCache.get(b.entityId);
return this.settings.tabSortKey === TabSortKey.NAME_ASC
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
});
}
private updateDatasources() {
@ -434,11 +435,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sourceIndex = 0;
let keyOffset = 0;
let latestKeyOffset = 0;
const entityLabelCache = new Map<string,string>();
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
if (this.datasources) {
const sortedDatasources = this.sortDatasources(this.datasources, entityLabelCache);
for (const datasource of sortedDatasources) {
for (const datasource of this.datasources) {
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
const source = {} as TimeseriesTableSource;
source.header = this.prepareHeader(datasource);
@ -455,7 +454,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
source.pageLink = new PageLink(pageSize, 0, null, sortOrder);
source.rowDataTemplate = {};
source.rowDataTemplate.Timestamp = null;
source.displayName = entityLabelCache.get(datasource.entityId);
source.displayName = this.getTabLabel(datasource);
if (this.showTimestamp) {
source.displayedColumns.push('0');
}
@ -475,6 +474,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}
}
if (this.sources.length) {
this.sortDatasources(this.sources);
this.sources.forEach((source, index) => {
this.prepareDisplayedColumn(index);
source.displayedColumns = this.displayedColumns[index].filter(value => value.display).map(value => value.def);

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

4
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -233,7 +233,9 @@
<div class="notification-content mini" [style.background-color]="preview.processedTemplates.MICROSOFT_TEAMS?.themeColor">
<div class="subject">{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}</div>
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }}
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button?.text }}</button>
@if (preview.processedTemplates.MICROSOFT_TEAMS.button?.enabled) {
<button mat-raised-button>{{ preview.processedTemplates.MICROSOFT_TEAMS.button?.text }}</button>
}
</div>
</section>
</div>

46
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html

@ -44,8 +44,8 @@
</mat-error>
</mat-form-field>
</div>
<div class="flex flex-row xs:flex-col gt-xs:gap-2">
<mat-form-field class="max-w-30% flex-full xs:max-w-full">
<div class="grid gt-sm:grid-cols-3 gt-sm:gap-4">
<mat-form-field class="mat-block">
<mat-label translate>notification.action-type</mat-label>
<mat-select formControlName="linkType">
<mat-option *ngFor="let actionButtonLinkType of actionButtonLinkTypes"
@ -54,32 +54,28 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1"
*ngIf="actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK; else dashboardSelector">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete
class="max-w-35% flex-full xs:max-w-full"
required
formControlName="dashboardId">
@if(actionButtonConfigForm.get('linkType').value === actionButtonLinkType.LINK) {
<mat-form-field class="gt-sm:col-span-2">
<mat-label translate>notification.link</mat-label>
<input matInput formControlName="link" required>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
} @else {
<tb-dashboard-autocomplete required formControlName="dashboardId">
</tb-dashboard-autocomplete>
<tb-dashboard-state-autocomplete class="max-w-35% flex-full xs:max-w-full"
[dashboardId]="actionButtonConfigForm.get('dashboardId').value"
<tb-dashboard-state-autocomplete [dashboardId]="actionButtonConfigForm.get('dashboardId').value"
formControlName="dashboardState">
</tb-dashboard-state-autocomplete>
</ng-template>
}
</div>
<mat-slide-toggle formControlName="setEntityIdInState" class="toggle"
*ngIf="actionButtonConfigForm.get('linkType').value === actionButtonLinkType.DASHBOARD">

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;

5
ui-ngx/src/assets/dashboard/api_usage.json

@ -99,7 +99,10 @@
"showTimestamp": true,
"displayPagination": true,
"defaultPageSize": 10,
"tabSortKey": "NAME_ASC"
"sortOrder": {
"property": "name",
"direction": "ASC"
}
},
"title": "{i18n:api-usage.exceptions}",
"dropShadow": true,

58
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": {
@ -1247,22 +1252,26 @@
"strategy": "Strategy",
"process-right-away": "Process right away",
"process-rule-chains": "Process via Rule Chains",
"processing-options": "Processing options",
"save-time-series": "Save to time series",
"save-database": "Save to database",
"save-latest-values": "Save to latest values",
"send-web-sockets": "Send to WebSockets",
"save-calculated-fields": "Send to Calculated fields",
"update-attributes-only-on-value-change": "Save attributes only if the value changes",
"ttl": "TTL",
"ttl-required": "TTL is required.",
"ttl-min": "Only 0 minimum TTL is allowed.",
"ttl": "Custom TTL",
"ttl-required": "TTL is required",
"ttl-min": "Only 0 minimum TTL is allowed",
"hint": {
"strategy": "Strategy",
"strategy": "Controls whether the result is processed immediately or sent to a rule chain for additional processing",
"processing-options": "Processing options",
"update-attributes-only-on-value-change": "Updates the attributes on every incoming message disregarding if their value has changed. Increases API usage and reduces performance.",
"update-attributes-only-on-value-change-enabled": "Updates the attributes only if their value has changed. If the value is not changed, no update to the attribute timestamp nor attribute change notification will be sent.",
"ttl": "If no value is present, it defaults to the TTL specified in the configuration. If the value is set to 0, the TTL from the tenant profile configuration will be applied."
"save-time-series": "Saves time series data to the ts_kv table in the database.",
"save-database": "Saves attribute data to the database.",
"save-latest-values": "Updates time series data in the ts_kv_latest table in the database if the new value is more recent.",
"send-web-sockets": "Notifies WebSocket subscriptions about updates to the attribute data.",
"save-calculated-fields": "Notifies calculated fields about updates to the attribute data.",
"ttl": "Defines the retention period for time series data. If disabled, the Tenant Profile TTL is used."
}
},
"aggregate-interval-type": "Aggregate interval type",
@ -1305,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.",
@ -1345,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": {
@ -1405,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",
@ -1441,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"
@ -1463,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",
@ -1481,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",
@ -1497,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",
@ -1639,6 +1653,8 @@
"documentation": "Documentation",
"time-left": "{{time}} left",
"output": "Output",
"sort-asc": "Ascending",
"sort-desc": "Descending",
"suffix": {
"s": "s",
"ms": "ms"
@ -1708,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",
@ -9538,8 +9554,6 @@
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode",
"disable-sorting": "Disable sorting",
"sort-by": "Sort tabs by",
"sort-asc": "Name Ascending",
"sort-desc": "Name Descending",
"sort-timestamp-option": "Created time"
},
"latest-chart": {

6
ui-ngx/src/form.scss

@ -257,6 +257,12 @@
flex: 1;
}
}
&.flex-lt-lg {
@media #{$mat-lt-lg} {
width: auto;
flex: 1;
}
}
}
.fixed-title-width {
min-width: 200px;

Loading…
Cancel
Save